2023 10 gg cda parsing fix 2 (#1451)

* fix xml output to deal with namespaces properly

* Improve rendering or xml_no_order

* add command line demo app

* fix CDA parsing error for sdtc:raceCode

* Fix error message

* Bump test case version

---------

Co-authored-by: Grahame Grieve <grahameg@gmail.ccom>
Co-authored-by: dotasek <dotasek.dev@gmail.com>
This commit is contained in:
Grahame Grieve 2023-10-05 01:42:29 +03:00 committed by GitHub
parent ff29b09083
commit 752ab7a3d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 416 additions and 11 deletions

View File

@ -0,0 +1,335 @@
package org.hl7.fhir.r4.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.List;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.context.SimpleWorkerContext;
import org.hl7.fhir.r4.formats.IParser.OutputStyle;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Property;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type;
import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.NpmPackage;
public class CmdLineApp {
private FHIRToolingClient client;
private String currentId;
private Resource currentResource;
private SimpleWorkerContext context;
private FHIRPathEngine fpe;
public static void main(String[] args) throws IOException, Exception {
new CmdLineApp().execute();
}
private void execute() throws IOException {
System.out.print("Loading...");
NpmPackage npm = new FilesystemPackageCacheManager(true).loadPackage("hl7.fhir.r4.core");
context = SimpleWorkerContext.fromPackage(npm);
fpe = new FHIRPathEngine(context);
System.out.println(" Done");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
genMenu();
boolean finished = false;
do {
System.out.print("> ");
String cmd = reader.readLine();
String[] p = cmd.split("\\s+");
try {
if (p.length == 1 && p[0].equals("x")) {
finished = true;
} else if (p.length == 1 && p[0].equals("?")) {
genMenu();
} else if (p.length >= 1 && p[0].equals("c")) {
if (p.length == 1) {
connectToServer("http://hapi.fhir.org/baseR4");
} else {
connectToServer(p[1]);
}
} else if (p.length >= 1 && p[0].equals("imm")) {
if (p.length == 1) {
if (currentResource == null || !(currentResource instanceof Patient)) {
throw new FHIRException("Current resource must be a patient for this command");
}
getImmunizations();
} else {
select("Immunization", p[1]);
}
} else if (p.length >= 1 && p[0].equals("cnd")) {
if (p.length == 1) {
if (currentResource == null || !(currentResource instanceof Patient)) {
throw new FHIRException("Current resource must be a patient for this command");
}
getConditions();
} else {
select("Condition", p[1]);
}
} else if (p.length >= 1 && p[0].equals("prc")) {
if (p.length == 1) {
if (currentResource == null || !(currentResource instanceof Patient)) {
throw new FHIRException("Current resource must be a patient for this command");
}
getProcedures();
} else {
select("Procedure", p[1]);
}
} else if (p.length >= 1 && p[0].equals("v")) {
if (p.length == 1) {
viewResource();
} else {
viewResource(p[1]);
}
} else if (p.length >= 2 && p[0].equals("s")) {
search(p);
} else if (p.length >= 2 && p[0].equals("p")) {
select("Patient", p[1]);
} else if (p.length == 3 && p[0].equals("e")) {
edit(p[1], p[2]);
} else {
System.out.println("Command unknown or not understood: "+cmd);
}
} catch (Exception e) {
System.out.println("Error executing command "+p[0]+": "+e.getMessage());
}
} while (!finished);
System.out.println("Finished!");
}
private void getImmunizations() {
Bundle bnd = client.search("Immunization", "?patient="+currentId);
System.out.println(""+bnd.getTotal()+" Immunizations found. Printing "+bnd.getEntry().size());
for (BundleEntryComponent be : bnd.getEntry()) {
Resource imm = be.getResource();
System.out.println(summary(imm));
}
}
private void getProcedures() {
Bundle bnd = client.search("Procedure", "?patient="+currentId);
System.out.println(""+bnd.getTotal()+" Procedures found. Printing "+bnd.getEntry().size());
for (BundleEntryComponent be : bnd.getEntry()) {
Resource imm = be.getResource();
System.out.println(summary(imm));
}
}
private void getConditions() {
Bundle bnd = client.search("Condition", "?patient="+currentId);
System.out.println(""+bnd.getTotal()+" Conditions found. Printing "+bnd.getEntry().size());
for (BundleEntryComponent be : bnd.getEntry()) {
Resource imm = be.getResource();
System.out.println(summary(imm));
}
}
private void edit(String path, String value) {
if (path.contains(".")) {
List<Base> list = fpe.evaluate(currentResource, fpe.parse(path.substring(0, path.lastIndexOf("."))));
if (list.size() == 1) {
path = path.substring(path.lastIndexOf(".")+1);
Property p = list.get(0).getNamedProperty(path);
Base b = makeValue(p, value);
list.get(0).setProperty(path, b);
} else {
throw new FHIRException("Unable to set value at "+path+": "+list.size()+" matches");
}
} else {
Property p = currentResource.getNamedProperty(path);
Base b = makeValue(p, value);
currentResource.setProperty(path, b);
}
currentResource = client.update(currentResource);
}
private Base makeValue(Property p, String value) {
switch (p.getTypeCode()) {
case "boolean" : return new BooleanType(value);
case "code" : return new CodeType(value);
case "string" : return new StringType(value);
}
throw new FHIRException("Unhandled type "+p.getTypeCode());
}
private void viewResource(String path) {
System.out.println("Current Resource, query = "+path);
List<Base> list = fpe.evaluate(currentResource, fpe.parse(path));
for (Base b : list) {
System.out.println(b.toString());
}
}
private void viewResource() throws IOException {
System.out.println("Current Resource:");
System.out.println(new JsonParser().setOutputStyle(OutputStyle.PRETTY).composeString(currentResource));
}
private void select(String type, String id) {
if (type.equals("Patient")) {
currentResource = client.fetchResource(Patient.class, id);
} else if (type.equals("Immunization")) {
currentResource = client.fetchResource(Immunization.class, id);
} else if (type.equals("Condition")) {
currentResource = client.fetchResource(Condition.class, id);
} else if (type.equals("Procedure")) {
currentResource = client.fetchResource(Procedure.class, id);
} else {
throw new FHIRException("Unhandled type "+type);
}
currentId = type+"/"+id;
System.out.println("Resource = "+currentId+" "+summary(currentResource));
}
private String summary(Resource r) {
if (r instanceof Patient) {
Patient pat = (Patient) r;
return pat.getIdBase()+" "+pat.getGender()+" "+pat.getBirthDateElement().asStringValue()+" "+name(pat);
}
if (r instanceof Immunization) {
Immunization imm = (Immunization) r;
return imm.getIdBase()+" "+imm.getOccurrenceDateTimeType().asStringValue()+" "+code(imm.getVaccineCode())+" "+imm.getLotNumber()+" ("+imm.getStatus()+")";
}
if (r instanceof Condition) {
Condition cnd = (Condition) r;
return cnd.getIdBase()+" "+code(cnd.getClinicalStatus())+" "+code(cnd.getVerificationStatus())+" "+code(cnd.getCode())+" "+cnd.getRecordedDateElement().asStringValue();
}
if (r instanceof Procedure) {
Procedure prc = (Procedure) r;
return prc.getIdBase()+" "+prc.getStatusElement().asStringValue()+" "+code(prc.getCode())+" "+code(prc.getCode())+" "+dt(prc.getPerformed());
}
return "??";
}
private String dt(Type type) {
if (type == null) {
return "";
}
if (type.isPrimitive()) {
return type.primitiveValue();
}
if (type instanceof Period) {
Period pd = (Period) type;
return pd.getStartElement().asStringValue()+" -> "+pd.getEndElement().asStringValue();
}
return "??";
}
private String code(CodeableConcept cc) {
for (Coding c : cc.getCoding()) {
if (c.hasSystem()) {
String d = c.hasDisplay() ? " ("+c.getDisplay()+")" : "";
if (c.hasCode()) {
switch (c.getSystem()) {
case "http://hl7.org/fhir/sid/cvx": return "CVX "+c.getCode()+d;
case "http://snomed.info/sct": return "SCT "+c.getCode()+d;
default:
if (Utilities.startsWithInList(c.getSystem(), "http://terminology.hl7.org")) {
return c.getCode();
} else {
throw new FHIRException("Unknown system "+c.getSystem());
}
}
}
}
}
for (Coding c : cc.getCoding()) {
if (c.hasCode()) {
return c.getCode();
}
}
if (cc.hasText()) {
return cc.getText();
}
return "";
}
private void search(String[] p) {
if (client == null) {
throw new FHIRException("Not connected to to a server");
}
String search = "?name="+p[1];
if (p.length > 2) {
search = search +"&gender="+p[2];
if (p.length > 3) {
search = search +"&dob="+p[3];
}
}
Bundle bnd = client.search("Patient", search);
System.out.println(""+bnd.getTotal()+" Patients found. Printing "+bnd.getEntry().size());
for (BundleEntryComponent be : bnd.getEntry()) {
Patient pat = (Patient) be.getResource();
System.out.println(summary(pat));
}
}
private String name(Patient pat) {
if (pat.getName().size() > 0) {
return name(pat.getName().get(0));
}
return null;
}
private String name(HumanName n) {
if (n.hasText()) {
return n.getText();
}
if (n.hasFamily()) {
if (n.hasGiven()) {
return n.getGiven().get(0)+" "+n.getFamily().toUpperCase();
} else {
return n.getFamily().toUpperCase();
}
}
return "??";
}
private void connectToServer(String url) throws URISyntaxException {
client = new FHIRToolingClient(url, "FHIR-Command-Line-App");
CapabilityStatement cs = client.getCapabilitiesStatementQuick();
System.out.println("Connected to "+url+": "+cs.getSoftware().getName()+".");
}
private void genMenu() {
System.out.println("Simple Client. Commands you can run:");
System.out.println(" c {url} - connect to a server");
System.out.println(" s {name} [{gender}] {{dob]} - find a patient record");
System.out.println(" p {id} - choose a patient record");
System.out.println(" v [{field}] - see a set of field(s) in the current resource, or the whole resource");
System.out.println(" e {field} {value} - edit a field in the current resource");
System.out.println(" imm [{id}] - get a list of the patient's immunizations, or use the resource for the id (then use e to edit it)");
System.out.println(" cnd [{id}] - get a list of the patient's conditions, or use the resource for the id (then use e to edit it)");
System.out.println(" prc [{id}] - get a list of the patient's procedures, or use the resource for the id (then use e to edit it)");
System.out.println(" ? - print this again");
System.out.println(" x - exit");
}
}

View File

@ -595,4 +595,23 @@ public class FHIRToolingClient {
public void setLanguage(String lang) {
this.acceptLang = lang;
}
public Bundle search(String type, String criteria) {
return fetchFeed(Utilities.pathURL(base, type+criteria));
}
public <T extends Resource> T fetchResource(Class<T> resourceClass, String id) {
org.hl7.fhir.r4.utils.client.network.ResourceRequest<Resource> result = null;
try {
result = client.issueGetResourceRequest(resourceAddress.resolveGetResource(resourceClass, id),
getPreferredResourceFormat(), generateHeaders(), resourceClass.getName()+"/"+id, TIMEOUT_NORMAL);
} catch (IOException e) {
throw new FHIRException(e);
}
if (result.isUnsuccessfulRequest()) {
throw new EFhirClientException("Server returned error code " + result.getHttpStatus(),
(OperationOutcome) result.getPayload());
}
return (T) result.getPayload();
}
}

View File

@ -91,6 +91,10 @@ public class ResourceAddress {
return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "/" + opName);
}
public <T extends Resource> URI resolveGetResource(Class<T> resourceClass, String id) {
return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "/" + id);
}
public <T extends Resource> URI resolveOperationUri(Class<T> resourceClass, String opName,
Map<String, String> parameters) {
return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "$" + opName),

View File

@ -61,6 +61,7 @@ import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r5.formats.FormatUtilities;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.ElementDefinition;
import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation;
import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.model.StructureDefinition;
@ -421,7 +422,11 @@ public class XmlParser extends ParserBase {
element.getChildren().add(n);
} else {
String npath = path+"/"+pathPrefix(child.getNamespaceURI())+child.getLocalName();
Element n = new Element(child.getLocalName(), property).markLocation(line(child, false), col(child, false)).setFormat(FhirFormat.XML);
String name = child.getLocalName();
if (!property.isChoice() && !name.equals(property.getName())) {
name = property.getName();
}
Element n = new Element(name, property).markLocation(line(child, false), col(child, false)).setFormat(FhirFormat.XML);
if (property.isList()) {
n.setPath(element.getPath()+"."+property.getName()+"["+repeatCount+"]");
} else {
@ -769,6 +774,10 @@ public class XmlParser extends ParserBase {
}
if (linkResolver != null)
xml.link(linkResolver.resolveProperty(element.getProperty()));
if (!xml.namespaceDefined(element.getProperty().getXmlNamespace())) {
String abbrev = makeNamespaceAbbrev(element.getProperty(), xml);
xml.namespace(element.getProperty().getXmlNamespace(), abbrev);
}
xml.enter(element.getProperty().getXmlNamespace(), elementName);
if (!root && element.getSpecial() != null) {
if (linkResolver != null)
@ -791,6 +800,37 @@ public class XmlParser extends ParserBase {
}
}
private String makeNamespaceAbbrev(Property property, IXMLWriter xml) {
// it's a cosmetic thing, but we're going to try to come up with a nice namespace
ElementDefinition ed = property.getDefinition();
String ns = property.getXmlNamespace();
String n = property.getXmlName();
String diff = property.getName().toLowerCase().replace(n.toLowerCase(), "");
if (!Utilities.noString(diff) && diff.length() <= 5 && Utilities.isToken(diff) && !xml.abbreviationDefined(diff)) {
return diff;
}
int i = ns.length()-1;
while (i > 0) {
if (Character.isAlphabetic(ns.charAt(i)) || Character.isDigit(ns.charAt(i))) {
i--;
} else {
break;
}
}
String tail = ns.substring(i+1);
if (!Utilities.noString(tail) && tail.length() <= 5 && Utilities.isToken(tail) && !xml.abbreviationDefined(tail)) {
return tail;
}
i = 0;
while (xml.abbreviationDefined("ns"+i)) {
i++;
}
return "ns"+i;
}
private String checkHeader(List<ValidationMessage> errors, InputStream stream) throws IOException {
try {
// the stream will either start with the UTF-8 BOF or with <xml

View File

@ -1482,6 +1482,11 @@ public class StructureDefinitionRenderer extends ResourceRenderer {
c.getPieces().add(gen.new Piece(null, translate("sd.table", "XML Namespace")+": ", null).addStyle("font-weight:bold"));
c.getPieces().add(gen.new Piece(null, definition.getExtensionString(ToolingExtensions.EXT_XML_NAMESPACE), null));
}
if (root && ToolingExtensions.readBoolExtension(profile, ToolingExtensions.EXT_XML_NO_ORDER)) {
if (!c.getPieces().isEmpty()) { c.addPiece(gen.new Piece("br")); }
c.getPieces().add(gen.new Piece(null, translate("sd.table", "XML Order")+": ", null).addStyle("font-weight:bold"));
c.getPieces().add(gen.new Piece(null, "The properties of this type can appear in any order in the XML", null));
}
if (definition.hasExtension(ToolingExtensions.EXT_JSON_EMPTY)) {
if (!c.getPieces().isEmpty()) { c.addPiece(gen.new Piece("br")); }
String code = ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_EMPTY);
@ -3755,7 +3760,7 @@ public class StructureDefinitionRenderer extends ResourceRenderer {
}
boolean no = root && ToolingExtensions.readBoolExtension(profile, ToolingExtensions.EXT_XML_NO_ORDER);
if (no) {
ret.tx("The children of this property can appear in any order in the XML.");
ret.tx("The children of this type can appear in any order in the XML.");
}
return ret;
}

View File

@ -249,7 +249,7 @@ Validation_VAL_Profile_MultipleMatches_other = Found multiple matching profiles
Validation_VAL_Profile_NoDefinition = No definition found for resource type ''{0}''
Validation_VAL_Profile_NoMatch = Unable to find a match for the specified profile among choices: {0}
Validation_VAL_Profile_NoSnapshot = StructureDefinition {0} has no snapshot - validation is against the snapshot, so it must be provided
Validation_VAL_Profile_NoType = The type of element {0} is not known, which is invalid. Valid types at this point are {1}
Validation_VAL_Profile_NoType = The type of element {0} is not known - it could not be determined from the information available. Valid types at this point are {1}
Validation_VAL_Profile_NotAllowed = This element is not allowed by the profile {0}
Validation_VAL_Profile_NotSlice = This element does not match any known slice {0} and slicing is CLOSED: {1}
Validation_VAL_Profile_OutOfOrder = As specified by profile {0}, Element ''{1}'' is out of order (found after {2})

View File

@ -6215,12 +6215,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (ed.getRepresentation().isEmpty()) { // ignore xml attributes
int count = 0;
List<ElementDefinition> slices = null;
if (ed.hasSlicing())
if (ed.hasSlicing()) {
slices = profileUtilities.getSliceList(profile, ed);
for (ElementInfo ei : children)
if (ei.definition == ed)
}
for (ElementInfo ei : children) {
if (ei.definition == ed) {
count++;
else if (slices != null) {
} else if (slices != null) {
for (ElementDefinition sed : slices) {
if (ei.definition == sed) {
count++;
@ -6228,6 +6229,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
}
}
}
if (ed.getMin() > 0) {
if (problematicPaths.contains(ed.getPath()))
hintPlural(errors, NO_RULE_DATE, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count >= ed.getMin(), count, I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMIN, profile.getVersionedUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), Integer.toString(ed.getMin()));
@ -6238,9 +6240,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
}
if (ed.hasMax() && !ed.getMax().equals("*")) {
if (problematicPaths.contains(ed.getPath()))
if (problematicPaths.contains(ed.getPath())) {
hintPlural(errors, NO_RULE_DATE, IssueType.NOTSUPPORTED, element.line(), element.col(), stack.getLiteralPath(), count <= Integer.parseInt(ed.getMax()), count, I18nConstants.VALIDATION_VAL_PROFILE_NOCHECKMAX, profile.getVersionedUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), ed.getMax());
else if (count > Integer.parseInt(ed.getMax())) {
} else if (count > Integer.parseInt(ed.getMax())) {
ok = rulePlural(errors, NO_RULE_DATE, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), false, count, I18nConstants.VALIDATION_VAL_PROFILE_MAXIMUM, profile.getVersionedUrl(), ed.getPath(), ed.getId(), ed.getSliceName(),ed.getLabel(), stack.getLiteralPath(), ed.getMax(), Integer.toString(count)) && ok;
}
}

View File

@ -20,7 +20,7 @@
<properties>
<guava_version>32.0.1-jre</guava_version>
<hapi_fhir_version>6.4.1</hapi_fhir_version>
<validator_test_case_version>1.4.7</validator_test_case_version>
<validator_test_case_version>1.4.8</validator_test_case_version>
<jackson_version>2.15.2</jackson_version>
<junit_jupiter_version>5.9.2</junit_jupiter_version>
<junit_platform_launcher_version>1.8.2</junit_platform_launcher_version>