work on ConceptMap infrastructure for cross-version analysis

This commit is contained in:
Grahame Grieve 2024-02-25 19:18:06 +11:00
parent 3a27e012be
commit c2756a24ef
10 changed files with 884 additions and 13 deletions

View File

@ -107,7 +107,7 @@ public abstract class FormatUtilities {
} }
public static boolean isValidId(String tail) { public static boolean isValidId(String tail) {
return tail.matches(ID_REGEX); return tail == null ? false : tail.matches(ID_REGEX);
} }
public static String makeId(String candidate) { public static String makeId(String candidate) {

View File

@ -596,6 +596,9 @@ public abstract class CanonicalResource extends DomainResource {
} }
public String present() { public String present() {
if (hasUserData("presentation")) {
return getUserString("presentation");
}
if (hasTitle()) if (hasTitle())
return getTitle(); return getTitle();
if (hasName()) if (hasName())

View File

@ -1777,10 +1777,23 @@ public class ConceptMap extends MetadataResource {
} }
public SourceElementComponent getOrAddElement(String code) {
for (SourceElementComponent e : getElement()) {
if (code.equals(e.getCode())) {
return e;
}
}
return addElement().setCode(code);
}
} }
@Block() @Block()
public static class SourceElementComponent extends BackboneElement implements IBaseBackboneElement { public static class SourceElementComponent extends BackboneElement implements IBaseBackboneElement {
@Override
public String toString() {
return "SourceElementComponent [code=" + code + ", display=" + display + ", noMap=" + noMap + "]";
}
/** /**
* Identity (code or path) or the element/item being mapped. * Identity (code or path) or the element/item being mapped.
*/ */
@ -2264,6 +2277,11 @@ public class ConceptMap extends MetadataResource {
@Block() @Block()
public static class TargetElementComponent extends BackboneElement implements IBaseBackboneElement { public static class TargetElementComponent extends BackboneElement implements IBaseBackboneElement {
@Override
public String toString() {
return "TargetElementComponent [code=" + code + ", relationship=" + relationship + "]";
}
/** /**
* Identity (code or path) or the element/item that the map refers to. * Identity (code or path) or the element/item that the map refers to.
*/ */

View File

@ -3829,6 +3829,17 @@ public class Enumerations {
default: return "?"; default: return "?";
} }
} }
public String getSymbol() {
switch (this) {
case RELATEDTO: return "-";
case EQUIVALENT: return "=";
case SOURCEISNARROWERTHANTARGET: return "<";
case SOURCEISBROADERTHANTARGET: return ">";
case NOTRELATEDTO: return "!=";
case NULL: return null;
default: return "?";
}
}
} }
public static class ConceptMapRelationshipEnumFactory implements EnumFactory<ConceptMapRelationship> { public static class ConceptMapRelationshipEnumFactory implements EnumFactory<ConceptMapRelationship> {

View File

@ -1150,6 +1150,15 @@ public class ValueSet extends MetadataResource {
} }
public boolean hasConcept(String code) {
for (ConceptReferenceComponent c : getConcept()) {
if (code.equals(c.getCode())) {
return true;
}
}
return false;
}
} }
@Block() @Block()

View File

@ -1,6 +1,9 @@
package org.hl7.fhir.r5.renderers; package org.hl7.fhir.r5.renderers;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -9,8 +12,10 @@ import java.util.Map;
import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.exceptions.FHIRFormatError;
import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.ConceptMap; import org.hl7.fhir.r5.model.ConceptMap;
import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent;
import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode;
import org.hl7.fhir.r5.model.ConceptMap.MappingPropertyComponent; import org.hl7.fhir.r5.model.ConceptMap.MappingPropertyComponent;
import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent; import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent;
import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent;
@ -19,14 +24,263 @@ import org.hl7.fhir.r5.model.ContactDetail;
import org.hl7.fhir.r5.model.ContactPoint; import org.hl7.fhir.r5.model.ContactPoint;
import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship;
import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.renderers.ConceptMapRenderer.MultipleMappingRow;
import org.hl7.fhir.r5.renderers.ConceptMapRenderer.MultipleMappingRowSorter;
import org.hl7.fhir.r5.renderers.ConceptMapRenderer.RenderMultiRowSortPolicy;
import org.hl7.fhir.r5.renderers.utils.RenderingContext; import org.hl7.fhir.r5.renderers.utils.RenderingContext;
import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext; import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.DebugUtilities;
import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlFluent;
import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.utilities.xhtml.XhtmlNode;
public class ConceptMapRenderer extends TerminologyRenderer { public class ConceptMapRenderer extends TerminologyRenderer {
public enum RenderMultiRowSortPolicy {
UNSORTED, FIRST_COL, LAST_COL
}
public interface IConceptMapInformationProvider {
public List<Coding> getMembers(String uri);
public String getLink(String system, String code);
}
public static class MultipleMappingRowSorter implements Comparator<MultipleMappingRow> {
private boolean first;
protected MultipleMappingRowSorter(boolean first) {
super();
this.first = first;
}
@Override
public int compare(MultipleMappingRow o1, MultipleMappingRow o2) {
String s1 = first ? o1.firstCode() : o1.lastCode();
String s2 = first ? o2.firstCode() : o2.lastCode();
return s1.compareTo(s2);
}
}
public static class Cell {
private String system;
private String code;
private String display;
private String relationship;
private String relComment;
public boolean renderedRel;
public boolean renderedCode;
private Cell clone;
protected Cell() {
super();
}
public Cell(String system, String code, String display) {
this.system = system;
this.code = code;
this.display = display;
}
public Cell(String system, String code, String relationship, String comment) {
this.system = system;
this.code = code;
this.relationship = relationship;
this.relComment = comment;
}
public boolean matches(String system, String code) {
return (system != null && system.equals(this.system)) && (code != null && code.equals(this.code));
}
public String present() {
if (system == null) {
return code;
} else {
return code; //+(clone == null ? "" : " (@"+clone.code+")");
}
}
public Cell copy(boolean clone) {
Cell res = new Cell();
res.system = system;
res.code = code;
res.display = display;
res.relationship = relationship;
res.relComment = relComment;
res.renderedRel = renderedRel;
res.renderedCode = renderedCode;
if (clone) {
res.clone = this;
}
return res;
}
@Override
public String toString() {
return relationship+" "+system + "#" + code + " \"" + display + "\"";
}
}
public static class MultipleMappingRowItem {
List<Cell> cells = new ArrayList<>();
@Override
public String toString() {
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (Cell cell : cells) {
if (cell.relationship != null) {
b.append(cell.relationship+cell.code);
} else {
b.append(cell.code);
}
}
return b.toString();
}
}
public static class MultipleMappingRow {
private List<MultipleMappingRowItem> rowSets = new ArrayList<>();
private MultipleMappingRow stickySource;
public MultipleMappingRow(int i, String system, String code, String display) {
MultipleMappingRowItem row = new MultipleMappingRowItem();
rowSets.add(row);
for (int c = 0; c < i; c++) {
row.cells.add(new Cell()); // blank cell spaces
}
row.cells.add(new Cell(system, code, display));
}
public MultipleMappingRow(MultipleMappingRow stickySource) {
this.stickySource = stickySource;
}
@Override
public String toString() {
CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
for (MultipleMappingRowItem rowSet : rowSets) {
b.append(""+rowSet.cells.size());
}
CommaSeparatedStringBuilder b2 = new CommaSeparatedStringBuilder(";");
for (MultipleMappingRowItem rowSet : rowSets) {
b2.append(rowSet.toString());
}
return ""+rowSets.size()+" ["+b.toString()+"] ("+b2.toString()+")";
}
public String lastCode() {
MultipleMappingRowItem first = rowSets.get(0);
for (int i = first.cells.size()-1; i >= 0; i--) {
if (first.cells.get(i).code != null) {
return first.cells.get(i).code;
}
}
return "";
}
public String firstCode() {
MultipleMappingRowItem first = rowSets.get(0);
for (int i = 0; i < first.cells.size(); i++) {
if (first.cells.get(i).code != null) {
return first.cells.get(i).code;
}
}
return "";
}
public void addSource(MultipleMappingRow sourceRow, List<MultipleMappingRow> rowList, ConceptMapRelationship relationship, String comment) {
// we already have a row, and we're going to collapse the rows on sourceRow into here, and add a matching terminus
assert sourceRow.rowSets.get(0).cells.size() == rowSets.get(0).cells.size()-1;
rowList.remove(sourceRow);
Cell template = rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1);
for (MultipleMappingRowItem row : sourceRow.rowSets) {
row.cells.add(new Cell(template.system, template.code, relationship.getSymbol(), comment));
}
rowSets.addAll(sourceRow.rowSets);
}
public void addTerminus() {
for (MultipleMappingRowItem row : rowSets) {
row.cells.add(new Cell(null, null, "X", null));
}
}
public void addTarget(String system, String code, ConceptMapRelationship relationship, String comment, List<MultipleMappingRow> sets, int colCount) {
if (rowSets.get(0).cells.size() == colCount+1) { // if it's already has a target for this col then we have to clone (and split) the rows
for (MultipleMappingRowItem row : rowSets) {
row.cells.add(new Cell(system, code, relationship.getSymbol(), comment));
}
} else {
MultipleMappingRow nrow = new MultipleMappingRow(this);
for (MultipleMappingRowItem row : rowSets) {
MultipleMappingRowItem n = new MultipleMappingRowItem();
for (int i = 0; i < row.cells.size()-1; i++) { // note to skip the last
n.cells.add(row.cells.get(i).copy(true));
}
n.cells.add(new Cell(system, code, relationship.getSymbol(), comment));
nrow.rowSets.add(n);
}
sets.add(sets.indexOf(this), nrow);
}
}
public String lastSystem() {
MultipleMappingRowItem first = rowSets.get(0);
for (int i = first.cells.size()-1; i >= 0; i--) {
if (first.cells.get(i).system != null) {
return first.cells.get(i).system;
}
}
return "";
}
public void addCopy(String system) {
for (MultipleMappingRowItem row : rowSets) {
row.cells.add(new Cell(system, lastCode(), "=", null));
}
}
public boolean alreadyHasMappings(int i) {
for (MultipleMappingRowItem row : rowSets) {
if (row.cells.size() > i+1) {
return true;
}
}
return false;
}
public Cell getLastSource(int i) {
for (MultipleMappingRowItem row : rowSets) {
return row.cells.get(i+1);
}
throw new Error("Should not get here"); // return null
}
public void cloneSource(int i, Cell cell) {
MultipleMappingRowItem row = new MultipleMappingRowItem();
rowSets.add(row);
for (int c = 0; c < i-1; c++) {
row.cells.add(new Cell()); // blank cell spaces
}
row.cells.add(cell.copy(true));
row.cells.add(rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1).copy(false));
}
}
public ConceptMapRenderer(RenderingContext context) { public ConceptMapRenderer(RenderingContext context) {
super(context); super(context);
} }
@ -475,4 +729,254 @@ public class ConceptMapRenderer extends TerminologyRenderer {
return null; return null;
} }
public static XhtmlNode renderMultipleMaps(String start, List<ConceptMap> maps, IConceptMapInformationProvider linker, RenderMultiRowSortPolicy sort) {
// 1+1 column for each provided map
List<MultipleMappingRow> rowSets = new ArrayList<>();
for (int i = 0; i < maps.size(); i++) {
populateRows(rowSets, maps.get(i), i, linker);
}
collateRows(rowSets);
if (sort != RenderMultiRowSortPolicy.UNSORTED) {
Collections.sort(rowSets, new MultipleMappingRowSorter(sort == RenderMultiRowSortPolicy.FIRST_COL));
}
XhtmlNode div = new XhtmlNode(NodeType.Element, "div");
XhtmlNode tbl = div.table("none").style("text-align: left; border-spacing: 0; padding: 5px");
XhtmlNode tr = tbl.tr();
styleCell(tr.td(), false, true, 5).b().tx(start);
for (ConceptMap map : maps) {
if (map.hasWebPath()) {
styleCell(tr.td(), false, true, 5).colspan(2).b().ah(map.getWebPath(), map.getVersionedUrl()).tx(map.present());
} else {
styleCell(tr.td(), false, true, 5).colspan(2).b().tx(map.present());
}
}
for (MultipleMappingRow row : rowSets) {
renderMultiRow(tbl, row, maps, linker);
}
return div;
}
private static void collateRows(List<MultipleMappingRow> rowSets) {
List<MultipleMappingRow> toDelete = new ArrayList<ConceptMapRenderer.MultipleMappingRow>();
for (MultipleMappingRow rowSet : rowSets) {
MultipleMappingRow tgt = rowSet.stickySource;
while (toDelete.contains(tgt)) {
tgt = tgt.stickySource;
}
if (tgt != null && rowSets.contains(tgt)) {
tgt.rowSets.addAll(rowSet.rowSets);
toDelete.add(rowSet);
}
}
rowSets.removeAll(toDelete);
}
private static void renderMultiRow(XhtmlNode tbl, MultipleMappingRow rows, List<ConceptMap> maps, IConceptMapInformationProvider linker) {
int rowCounter = 0;
for (MultipleMappingRowItem row : rows.rowSets) {
XhtmlNode tr = tbl.tr();
boolean first = true;
int cellCounter = 0;
Cell last = null;
for (Cell cell : row.cells) {
if (first) {
if (!cell.renderedCode) {
int c = 1;
for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) {
rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true;
c++;
} else {
break;
}
}
if (cell.code == null) {
styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee");
} else {
String link = linker.getLink(cell.system, cell.code);
XhtmlNode x = null;
if (link != null) {
x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link);
} else {
x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c);
}
// if (cell.clone != null) {
// x.style("color: grey");
// }
x.tx(cell.present());
}
}
first = false;
} else {
if (!cell.renderedRel) {
int c = 1;
for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
if ((cell.relationship != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.relationship.equals(rows.rowSets.get(i).cells.get(cellCounter).relationship)) &&
(cell.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) &&
(last.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter-1).code))) {
rows.rowSets.get(i).cells.get(cellCounter).renderedRel = true;
c++;
} else {
break;
}
}
if (last.code == null || cell.code == null) {
styleCell(tr.td(), rowCounter == 0, true, 5).style("background-color: #eeeeee");
} else if (cell.relationship != null) {
styleCell(tr.tdW(16), rowCounter == 0, true, 0).attributeNN("title", cell.relComment).rowspan(c).style("background-color: LightGrey; text-align: center; vertical-align: middle; color: white").tx(cell.relationship);
} else {
styleCell(tr.tdW(16), rowCounter == 0, false, 0).rowspan(c);
}
}
if (!cell.renderedCode) {
int c = 1;
for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) {
rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true;
c++;
} else {
break;
}
}
if (cell.code == null) {
styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee");
} else {
String link = linker.getLink(cell.system, cell.code);
XhtmlNode x = null;
if (link != null) {
x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link);
} else {
x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c);
}
// if (cell.clone != null) {
// x.style("color: grey");
// }
x.tx(cell.present());
}
}
}
last = cell;
cellCounter++;
}
rowCounter++;
}
}
private static XhtmlNode styleCell(XhtmlNode td, boolean firstrow, boolean sides, int padding) {
if (firstrow) {
td.style("vertical-align: middle; border-top: 1px solid black; padding: "+padding+"px");
} else {
td.style("vertical-align: middle; border-top: 1px solid LightGrey; padding: "+padding+"px");
}
if (sides) {
td.style("border-left: 1px solid LightGrey; border-right: 2px solid LightGrey");
}
return td;
}
private static void populateRows(List<MultipleMappingRow> rowSets, ConceptMap map, int i, IConceptMapInformationProvider linker) {
// if we can resolve the value set, we create entries for it
if (map.hasSourceScope()) {
List<Coding> codings = linker.getMembers(map.getSourceScope().primitiveValue());
if (codings != null) {
for (Coding c : codings) {
MultipleMappingRow row = i == 0 ? null : findExistingRowBySource(rowSets, c.getSystem(), c.getCode(), i);
if (row == null) {
row = new MultipleMappingRow(i, c.getSystem(), c.getCode(), c.getDisplay());
rowSets.add(row);
}
}
}
}
for (ConceptMapGroupComponent grp : map.getGroup()) {
for (SourceElementComponent src : grp.getElement()) {
MultipleMappingRow row = findExistingRowBySource(rowSets, grp.getSource(), src.getCode(), i);
if (row == null) {
row = new MultipleMappingRow(i, grp.getSource(), src.getCode(), src.getDisplay());
rowSets.add(row);
}
if (src.getNoMap()) {
row.addTerminus();
} else {
List<TargetElementComponent> todo = new ArrayList<>();
for (TargetElementComponent tgt : src.getTarget()) {
MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), tgt.getCode(), i);
if (trow == null) {
row.addTarget(grp.getTarget(), tgt.getCode(), tgt.getRelationship(), tgt.getComment(), rowSets, i);
} else {
todo.add(tgt);
}
}
// we've already got a mapping to these targets. So we gather them under the one mapping - but do this after the others are done
for (TargetElementComponent t : todo) {
MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), t.getCode(), i);
if (row.alreadyHasMappings(i)) {
// src is already mapped, and so is target, and now we need to map src to target too
// we have to clone src, but we only clone the last
trow.cloneSource(i, row.getLastSource(i));
} else {
trow.addSource(row, rowSets, t.getRelationship(), t.getComment());
}
}
}
}
boolean copy = grp.hasUnmapped() && grp.getUnmapped().getMode() == ConceptMapGroupUnmappedMode.USESOURCECODE;
if (copy) {
for (MultipleMappingRow row : rowSets) {
if (row.rowSets.get(0).cells.size() == i && row.lastSystem().equals(grp.getSource())) {
row.addCopy(grp.getTarget());
}
}
}
}
for (MultipleMappingRow row : rowSets) {
if (row.rowSets.get(0).cells.size() == i) {
row.addTerminus();
}
}
if (map.hasTargetScope()) {
List<Coding> codings = linker.getMembers(map.getTargetScope().primitiveValue());
if (codings != null) {
for (Coding c : codings) {
MultipleMappingRow row = findExistingRowByTarget(rowSets, c.getSystem(), c.getCode(), i);
if (row == null) {
row = new MultipleMappingRow(i+1, c.getSystem(), c.getCode(), c.getDisplay());
rowSets.add(row);
} else {
for (MultipleMappingRowItem cells : row.rowSets) {
Cell last = cells.cells.get(cells.cells.size() -1);
if (last.system != null && last.system.equals(c.getSystem()) && last.code.equals(c.getCode()) && last.display == null) {
last.display = c.getDisplay();
}
}
}
}
}
}
}
private static MultipleMappingRow findExistingRowByTarget(List<MultipleMappingRow> rows, String system, String code, int i) {
for (MultipleMappingRow row : rows) {
for (MultipleMappingRowItem cells : row.rowSets) {
if (cells.cells.size() > i + 1 && cells.cells.get(i+1).matches(system, code)) {
return row;
}
}
}
return null;
}
private static MultipleMappingRow findExistingRowBySource(List<MultipleMappingRow> rows, String system, String code, int i) {
for (MultipleMappingRow row : rows) {
for (MultipleMappingRowItem cells : row.rowSets) {
if (cells.cells.size() > i && cells.cells.get(i).matches(system, code)) {
return row;
}
}
}
return null;
}
} }

View File

@ -33,6 +33,7 @@ package org.hl7.fhir.r5.terminologies;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
@ -206,6 +207,11 @@ public class CodeSystemUtilities extends TerminologyUtilities {
return false; return false;
} }
public static boolean isNotSelectable(CodeSystem cs, String code) {
ConceptDefinitionComponent cd = findCode(cs.getConcept(), code);
return cd == null ? false : isNotSelectable(cs, cd);
}
public static void setNotSelectable(CodeSystem cs, ConceptDefinitionComponent concept) throws FHIRFormatError { public static void setNotSelectable(CodeSystem cs, ConceptDefinitionComponent concept) throws FHIRFormatError {
defineNotSelectableProperty(cs); defineNotSelectableProperty(cs);
ConceptPropertyComponent p = getProperty(concept, "notSelectable"); ConceptPropertyComponent p = getProperty(concept, "notSelectable");
@ -476,6 +482,28 @@ public class CodeSystemUtilities extends TerminologyUtilities {
return null; return null;
} }
public static List<ConceptDefinitionComponent> findCodeWithParents(List<ConceptDefinitionComponent> parents, List<ConceptDefinitionComponent> list, String code) {
for (ConceptDefinitionComponent c : list) {
if (c.hasCode() && c.getCode().equals(code)) {
return addToList(parents, c);
}
List<ConceptDefinitionComponent> s = findCodeWithParents(addToList(parents, c), c.getConcept(), code);
if (s != null)
return s;
}
return null;
}
private static List<ConceptDefinitionComponent> addToList(List<ConceptDefinitionComponent> parents, ConceptDefinitionComponent c) {
List<ConceptDefinitionComponent> res = new ArrayList<CodeSystem.ConceptDefinitionComponent>();
if (parents != null) {
res.addAll(parents);
}
res.add(c);
return res;
}
public static ConceptDefinitionComponent findCodeOrAltCode(List<ConceptDefinitionComponent> list, String code, String use) { public static ConceptDefinitionComponent findCodeOrAltCode(List<ConceptDefinitionComponent> list, String code, String use) {
for (ConceptDefinitionComponent c : list) { for (ConceptDefinitionComponent c : list) {
if (c.hasCode() && c.getCode().equals(code)) if (c.hasCode() && c.getCode().equals(code))
@ -928,5 +956,35 @@ public class CodeSystemUtilities extends TerminologyUtilities {
return v.primitiveValue(); return v.primitiveValue();
} }
} }
public static Boolean subsumes(CodeSystem cs, String pc, String cc) {
if (pc.equals(cc)) {
return true;
}
List<ConceptDefinitionComponent> child = findCodeWithParents(null, cs.getConcept(), cc);
for (ConceptDefinitionComponent item : child) {
if (pc.equals(item.getCode())) {
return true;
}
}
return false;
}
public static Set<String> codes(CodeSystem cs) {
Set<String> res = new HashSet<>();
addCodes(res, cs.getConcept());
return res;
}
private static void addCodes(Set<String> res, List<ConceptDefinitionComponent> list) {
for (ConceptDefinitionComponent cd : list) {
if (cd.hasCode()) {
res.add(cd.getCode());
}
if (cd.hasConcept()) {
addCodes(res, cd.getConcept());
}
}
}
} }

View File

@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Coding;
@ -16,6 +17,7 @@ import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent;
import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent; import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent;
import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship;
import org.hl7.fhir.r5.terminologies.ConceptMapUtilities.ConceptMapElementSorter; import org.hl7.fhir.r5.terminologies.ConceptMapUtilities.ConceptMapElementSorter;
import org.hl7.fhir.r5.terminologies.ConceptMapUtilities.ElementMappingPair;
import org.hl7.fhir.r5.model.Identifier; import org.hl7.fhir.r5.model.Identifier;
import org.hl7.fhir.r5.model.Meta; import org.hl7.fhir.r5.model.Meta;
import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.model.UriType;
@ -23,6 +25,36 @@ import org.hl7.fhir.r5.model.ValueSet;
public class ConceptMapUtilities { public class ConceptMapUtilities {
public static class TargetSorter implements Comparator<TargetElementComponent> {
@Override
public int compare(TargetElementComponent o1, TargetElementComponent o2) {
return o1.getCode().compareTo(o2.getCode());
}
}
public static class ElementSorter implements Comparator<SourceElementComponent> {
@Override
public int compare(SourceElementComponent o1, SourceElementComponent o2) {
return o1.getCode().compareTo(o2.getCode());
}
}
public static class ElementMappingPair {
private SourceElementComponent src;
private TargetElementComponent tgt;
public ElementMappingPair(SourceElementComponent src, TargetElementComponent tgt) {
this.src = src;
this.tgt = tgt;
}
}
public static class TranslatedCode { public static class TranslatedCode {
private String code; private String code;
private ConceptMapRelationship relationship; private ConceptMapRelationship relationship;
@ -37,7 +69,7 @@ public class ConceptMapUtilities {
public ConceptMapRelationship getRelationship() { public ConceptMapRelationship getRelationship() {
return relationship; return relationship;
} }
} }
public static class ConceptMapElementSorter implements Comparator<SourceElementComponent> { public static class ConceptMapElementSorter implements Comparator<SourceElementComponent> {
@ -287,5 +319,215 @@ public class ConceptMapUtilities {
} }
return null; return null;
} }
public static boolean checkReciprocal(ConceptMap left, ConceptMap right, List<String> issues) {
boolean altered = false;
if (!Base.compareDeep(left.getTargetScope(), right.getSourceScope(), true)) {
issues.add("scopes are not reciprocal: "+left.getTargetScope()+" vs "+right.getSourceScope());
}
if (!Base.compareDeep(left.getSourceScope(), right.getTargetScope(), true)) {
issues.add("scopes are not reciprocal: "+left.getSourceScope()+" vs "+right.getTargetScope());
}
if (left.getGroup().size() != right.getGroup().size()) {
issues.add("group count mismatch: "+left.getGroup().size()+" vs "+right.getGroup().size());
}
for (ConceptMapGroupComponent gl : left.getGroup()) {
ConceptMapGroupComponent gr = findMatchingGroup(right.getGroup(), gl.getTarget(), gl.getSource());
if (gr == null) {
issues.add("left maps from "+gl.getSource()+" to "+gl.getTarget()+" but right has no matching reverse map");
} else {
for (SourceElementComponent srcL : gl.getElement()) {
if (!"CHECK!".equals(srcL.getCode())) {
if (!srcL.getNoMap()) {
for (TargetElementComponent tgtL : srcL.getTarget()) {
List<ElementMappingPair> pairs = getMappings(gr, tgtL.getCode(), srcL.getCode());
switch (tgtL.getRelationship()) {
case EQUIVALENT:
if (pairs.isEmpty()) {
gr.getOrAddElement(tgtL.getCode()).addTarget().setCode(srcL.getCode()).setRelationship(ConceptMapRelationship.EQUIVALENT);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.EQUIVALENT) {
issues.add("Left map says that "+srcL.getCode()+" is equivalent to "+tgtL.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case RELATEDTO:
if (pairs.isEmpty()) {
gr.getOrAddElement(tgtL.getCode()).addTarget().setCode(srcL.getCode()).setRelationship(ConceptMapRelationship.RELATEDTO);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.EQUIVALENT && pair.tgt.getRelationship() != ConceptMapRelationship.RELATEDTO) {
issues.add("Left map says that "+srcL.getCode()+" is equivalent to "+tgtL.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case SOURCEISBROADERTHANTARGET:
if (pairs.isEmpty()) {
gr.getOrAddElement(tgtL.getCode()).addTarget().setCode(srcL.getCode()).setRelationship(ConceptMapRelationship.SOURCEISNARROWERTHANTARGET);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.SOURCEISNARROWERTHANTARGET) {
issues.add("Left map says that "+srcL.getCode()+" is broader than "+tgtL.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case SOURCEISNARROWERTHANTARGET:
if (pairs.isEmpty()) {
gr.getOrAddElement(tgtL.getCode()).addTarget().setCode(srcL.getCode()).setRelationship(ConceptMapRelationship.SOURCEISBROADERTHANTARGET);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.SOURCEISBROADERTHANTARGET) {
issues.add("Left map says that "+srcL.getCode()+" is narrower than "+tgtL.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case NOTRELATEDTO:
for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.NOTRELATEDTO) {
issues.add("Left map says that "+srcL.getCode()+" is not related to "+tgtL.getCode()+" but a reverse relationship exists with type "+pair.tgt.getRelationship().toCode());
}
}
break;
}
}
} else {
for (SourceElementComponent srcR : gr.getElement()) {
for (TargetElementComponent tgtR : srcR.getTarget()) {
if (srcL.getCode().equals(tgtR.getCode())) {
issues.add("Left map says that there is no relationship for "+srcL.getCode()+" but right map has a "+tgtR.getRelationship().toCode()+" mapping to it from "+srcR.getCode());
}
}
}
}
}
}
for (SourceElementComponent srcR : gr.getElement()) {
if (!"CHECK!".equals(srcR.getCode())) {
if (!srcR.getNoMap()) {
for (TargetElementComponent tgtR : srcR.getTarget()) {
List<ElementMappingPair> pairs = getMappings(gl, tgtR.getCode(), srcR.getCode());
switch (tgtR.getRelationship()) {
case EQUIVALENT:
if (pairs.isEmpty()) {
gl.getOrAddElement(tgtR.getCode()).addTarget().setCode(srcR.getCode()).setRelationship(ConceptMapRelationship.EQUIVALENT);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.EQUIVALENT) {
issues.add("Right map says that "+srcR.getCode()+" is equivalent to "+tgtR.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case RELATEDTO:
if (pairs.isEmpty()) {
gl.getOrAddElement(tgtR.getCode()).addTarget().setCode(srcR.getCode()).setRelationship(ConceptMapRelationship.RELATEDTO);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.EQUIVALENT && pair.tgt.getRelationship() != ConceptMapRelationship.RELATEDTO) {
issues.add("Right map says that "+srcR.getCode()+" is equivalent to "+tgtR.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case SOURCEISBROADERTHANTARGET:
if (pairs.isEmpty()) {
gl.getOrAddElement(tgtR.getCode()).addTarget().setCode(srcR.getCode()).setRelationship(ConceptMapRelationship.SOURCEISNARROWERTHANTARGET);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.SOURCEISNARROWERTHANTARGET) {
issues.add("Right map says that "+srcR.getCode()+" is broader than "+tgtR.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case SOURCEISNARROWERTHANTARGET:
if (pairs.isEmpty()) {
gl.getOrAddElement(tgtR.getCode()).addTarget().setCode(srcR.getCode()).setRelationship(ConceptMapRelationship.SOURCEISBROADERTHANTARGET);
altered = true;
} else for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.SOURCEISBROADERTHANTARGET) {
issues.add("Right map says that "+srcR.getCode()+" is narrower than "+tgtR.getCode()+" but the reverse relationship has type "+pair.tgt.getRelationship().toCode());
}
}
break;
case NOTRELATEDTO:
for (ElementMappingPair pair : pairs) {
if (pair.tgt.getRelationship() != ConceptMapRelationship.NOTRELATEDTO) {
issues.add("Right map says that "+srcR.getCode()+" is not related to "+tgtR.getCode()+" but a reverse relationship exists with type "+pair.tgt.getRelationship().toCode());
}
}
break;
}
}
} else {
for (SourceElementComponent srcL : gr.getElement()) {
for (TargetElementComponent tgtL : srcL.getTarget()) {
if (srcR.getCode().equals(tgtL.getCode())) {
issues.add("Right map says that there is no relationship for "+srcR.getCode()+" but right map has a "+tgtL.getRelationship().toCode()+" mapping to it from "+srcL.getCode());
}
}
}
}
}
}
}
}
return altered;
}
private static List<ElementMappingPair> getMappings(ConceptMapGroupComponent g, String source, String target) {
List<ElementMappingPair> res = new ArrayList<ConceptMapUtilities.ElementMappingPair>();
for (SourceElementComponent src : g.getElement()) {
for (TargetElementComponent tgt : src.getTarget()) {
if (source.equals(src.getCode()) && target.equals(tgt.getCode())) {
res.add(new ElementMappingPair(src, tgt));
}
}
}
return res;
}
private static ConceptMapGroupComponent findMatchingGroup(List<ConceptMapGroupComponent> groups, String source, String target) {
for (ConceptMapGroupComponent g : groups) {
if (source.equals(g.getSource()) && target.equals(g.getTarget())) {
return g;
}
}
return null;
}
/**
*
* @param cmF
* @return true if all the maps simply map to the same code
*/
public static boolean isUnityMap(ConceptMap cm) {
for (ConceptMapGroupComponent grp : cm.getGroup()) {
for (SourceElementComponent src : grp.getElement()) {
if (src.hasNoMap()) {
return false;
}
if (src.getTarget().size() != 1) {
return false;
}
if (src.getTargetFirstRep().getRelationship() != ConceptMapRelationship.EQUIVALENT && src.getTargetFirstRep().getRelationship() != ConceptMapRelationship.RELATEDTO) {
return false;
}
if (!src.getCode().equals(src.getTargetFirstRep().getCode())) {
return false;
}
}
}
return true;
}
public static int mapCount(ConceptMap cm) {
int i = 0;
for (ConceptMapGroupComponent grp : cm.getGroup()) {
for (SourceElementComponent src : grp.getElement()) {
i = i + src.getTarget().size();
}
}
return i;
}
} }

View File

@ -479,4 +479,25 @@ public class ValueSetUtilities extends TerminologyUtilities {
return false; return false;
} }
public static Set<String> codes(ValueSet vs, CodeSystem cs) {
Set<String> res = new HashSet<>();
for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
if (inc.getSystem().equals(cs.getUrl())) {
addCodes(res, inc, cs.getConcept());
}
}
return res;
}
private static void addCodes(Set<String> res, ConceptSetComponent inc, List<ConceptDefinitionComponent> list) {
for (ConceptDefinitionComponent cd : list) {
if (cd.hasCode() && (!inc.hasConcept() || inc.hasConcept(cd.getCode()))) {
res.add(cd.getCode());
}
if (cd.hasConcept()) {
addCodes(res, inc, cd.getConcept());
}
}
}
} }

View File

@ -838,7 +838,7 @@ public class ValueSetValidator extends ValueSetProcessBase {
} }
private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) { private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) {
ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), allAltCodes); ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), cs.getCaseSensitive(), allAltCodes);
if (cc == null) { if (cc == null) {
cc = findSpecialConcept(code, cs); cc = findSpecialConcept(code, cs);
} }
@ -850,6 +850,11 @@ public class ValueSetValidator extends ValueSetProcessBase {
String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_VERSION, code.getCode(), cs.getUrl(), cs.getVersion()); String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_VERSION, code.getCode(), cs.getUrl(), cs.getVersion());
return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.InvalidCode, null)); return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.InvalidCode, null));
} }
} else {
if (!cc.getCode().equals(code.getCode())) {
String msg = context.formatMessage(I18nConstants.CODE_CASE_DIFFERENCE, code.getCode(), cc.getCode(), cs.getVersionedUrl());
info.addIssue(makeIssue(IssueSeverity.INFORMATION, IssueType.BUSINESSRULE, path+".code", msg, OpIssueCode.CodeRule, null));
}
} }
Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs)); Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs));
if (vcc != null) { if (vcc != null) {
@ -1001,19 +1006,19 @@ public class ValueSetValidator extends ValueSetProcessBase {
return true; return true;
} }
private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, AlternateCodesProcessingRules altCodeRules) { private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) {
opContext.deadCheck(); opContext.deadCheck();
if (code.equals(concept.getCode())) { if (code.equals(concept.getCode())) {
return concept; return concept;
} }
ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, altCodeRules); ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, caseSensitive, altCodeRules);
if (cc != null) { if (cc != null) {
return cc; return cc;
} }
if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
for (ConceptDefinitionComponent c : children) { for (ConceptDefinitionComponent c : children) {
cc = findCodeInConcept(c, code, altCodeRules); cc = findCodeInConcept(c, code, caseSensitive, altCodeRules);
if (cc != null) { if (cc != null) {
return cc; return cc;
} }
@ -1022,15 +1027,15 @@ public class ValueSetValidator extends ValueSetProcessBase {
return null; return null;
} }
private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, AlternateCodesProcessingRules altCodeRules) { private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) {
for (ConceptDefinitionComponent cc : concept) { for (ConceptDefinitionComponent cc : concept) {
if (code.equals(cc.getCode())) { if (code.equals(cc.getCode()) || (!caseSensitive && (code.equalsIgnoreCase(cc.getCode())))) {
return cc; return cc;
} }
if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) { if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
return cc; return cc;
} }
ConceptDefinitionComponent c = findCodeInConcept(cc, code, altCodeRules); ConceptDefinitionComponent c = findCodeInConcept(cc, code, caseSensitive, altCodeRules);
if (c != null) { if (c != null) {
return c; return c;
} }
@ -1122,7 +1127,7 @@ public class ValueSetValidator extends ValueSetProcessBase {
} }
} }
} else { } else {
ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, allAltCodes); ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, cs.getCaseSensitive(), allAltCodes);
if (cc != null) { if (cc != null) {
sys.add(vsi.getSystem()); sys.add(vsi.getSystem());
} }
@ -1409,11 +1414,11 @@ public class ValueSetValidator extends ValueSetProcessBase {
if (!excludeRoot && code.equals(f.getValue())) { if (!excludeRoot && code.equals(f.getValue())) {
return true; return true;
} }
ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), altCodeParams); ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), cs.getCaseSensitive(), altCodeParams);
if (cc == null) { if (cc == null) {
return false; return false;
} }
ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, altCodeParams); ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, cs.getCaseSensitive(), altCodeParams);
return cc2 != null && cc2 != cc; return cc2 != null && cc2 != cc;
} }