From 54dcbe8a9fb5efa48dce68128b1f3ed2fac10f97 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 4 Oct 2023 18:36:52 +0300 Subject: [PATCH] add command line demo app --- .../org/hl7/fhir/r4/utils/CmdLineApp.java | 335 ++++++++++++++++++ .../r4/utils/client/FHIRToolingClient.java | 19 + .../fhir/r4/utils/client/ResourceAddress.java | 4 + 3 files changed, 358 insertions(+) create mode 100644 org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/CmdLineApp.java diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/CmdLineApp.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/CmdLineApp.java new file mode 100644 index 000000000..f2778934d --- /dev/null +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/CmdLineApp.java @@ -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 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 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"); + } +} diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java index 95028e204..3b3b2ad46 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/FHIRToolingClient.java @@ -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 fetchResource(Class resourceClass, String id) { + org.hl7.fhir.r4.utils.client.network.ResourceRequest 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(); + } } diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/ResourceAddress.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/ResourceAddress.java index 3b11ed94f..bffaa5900 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/ResourceAddress.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/client/ResourceAddress.java @@ -91,6 +91,10 @@ public class ResourceAddress { return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "/" + opName); } + public URI resolveGetResource(Class resourceClass, String id) { + return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "/" + id); + } + public URI resolveOperationUri(Class resourceClass, String opName, Map parameters) { return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "$" + opName),