001package org.hl7.fhir.r4.utils;
002
003import java.io.BufferedReader;
004import java.io.IOException;
005import java.io.InputStreamReader;
006import java.net.URISyntaxException;
007import java.util.List;
008
009import org.hl7.fhir.exceptions.FHIRException;
010import org.hl7.fhir.r4.context.SimpleWorkerContext;
011import org.hl7.fhir.r4.fhirpath.FHIRPathEngine;
012import org.hl7.fhir.r4.formats.IParser.OutputStyle;
013import org.hl7.fhir.r4.formats.JsonParser;
014import org.hl7.fhir.r4.model.Base;
015import org.hl7.fhir.r4.model.BooleanType;
016import org.hl7.fhir.r4.model.Bundle;
017import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
018import org.hl7.fhir.r4.model.CapabilityStatement;
019import org.hl7.fhir.r4.model.CodeSystem;
020import org.hl7.fhir.r4.model.CodeType;
021import org.hl7.fhir.r4.model.CodeableConcept;
022import org.hl7.fhir.r4.model.Coding;
023import org.hl7.fhir.r4.model.Condition;
024import org.hl7.fhir.r4.model.DateType;
025import org.hl7.fhir.r4.model.HumanName;
026import org.hl7.fhir.r4.model.Immunization;
027import org.hl7.fhir.r4.model.Parameters;
028import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
029import org.hl7.fhir.r4.model.Patient;
030import org.hl7.fhir.r4.model.Period;
031import org.hl7.fhir.r4.model.Procedure;
032import org.hl7.fhir.r4.model.Property;
033import org.hl7.fhir.r4.model.Resource;
034import org.hl7.fhir.r4.model.StringType;
035import org.hl7.fhir.r4.model.Type;
036import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
037import org.hl7.fhir.utilities.Utilities;
038import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
039import org.hl7.fhir.utilities.npm.NpmPackage;
040
041public class CmdLineApp {
042
043  private FHIRToolingClient client;
044  private String currentId;
045  private Resource currentResource;
046  private SimpleWorkerContext context;
047  private FHIRPathEngine fpe;
048
049  public static void main(String[] args) throws IOException, Exception {
050    new CmdLineApp().execute();
051  }
052
053  private void execute() throws IOException {
054    System.out.print("Loading...");
055    NpmPackage npm = new FilesystemPackageCacheManager.Builder().build().loadPackage("hl7.fhir.r4.core");
056    context = SimpleWorkerContext.fromPackage(npm);
057    fpe = new FHIRPathEngine(context);
058    System.out.println(" Done");
059    BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
060    genMenu();
061    boolean finished = false;
062    do {
063      System.out.print("> ");
064      String cmd = reader.readLine();
065      String[] p = cmd.split("\\s+");
066      try {
067        if (p.length == 1 && p[0].equals("x")) {
068          finished = true;
069        } else if (p.length == 1 && p[0].equals("?")) {
070          genMenu();
071        } else if (p.length == 1 && p[0].equals("?tx")) {
072          genMenuTx();
073        } else if (p.length >= 1 && p[0].equals("c")) {
074          if (p.length == 1) {
075            connectToServer("http://hapi.fhir.org/baseR4");
076          } else {
077            connectToServer(p[1]);
078          }
079        } else if (p.length >= 1 && p[0].equals("imm")) {
080          if (p.length == 1) {
081            if (currentResource == null || !(currentResource instanceof Patient)) {
082              throw new FHIRException("Current resource must be a patient for this command");
083            }
084            getImmunizations();
085          } else {
086            select("Immunization", p[1]);
087          }
088        } else if (p.length >= 1 && p[0].equals("cnd")) {
089          if (p.length == 1) {
090            if (currentResource == null || !(currentResource instanceof Patient)) {
091              throw new FHIRException("Current resource must be a patient for this command");
092            }
093            getConditions();
094          } else {
095            select("Condition", p[1]);
096          }
097        } else if (p.length >= 1 && p[0].equals("prc")) {
098          if (p.length == 1) {
099            if (currentResource == null || !(currentResource instanceof Patient)) {
100              throw new FHIRException("Current resource must be a patient for this command");
101            }
102            getProcedures();
103          } else {
104            select("Procedure", p[1]);
105          }
106        } else if (p.length >= 1 && p[0].equals("v")) {
107          if (p.length == 1) {
108            viewResource();
109          } else {
110            viewResource(p[1]);
111          }
112        } else if (p.length >= 2 && p[0].equals("s")) {
113          search(p);
114        } else if (p.length >= 2 && p[0].equals("p")) {
115          select("Patient", p[1]);
116        } else if (p.length == 3 && p[0].equals("e")) {
117          edit(p[1], p[2]);
118        } else if (p.length > 3 && p[0].equals("tx")) {
119          tx(p);
120        } else {
121          System.out.println("Command unknown or not understood: "+cmd);
122        }
123      } catch (Exception e) {
124        System.out.println("Error executing command "+p[0]+": "+e.getMessage());
125      }
126    } while (!finished);
127    
128    System.out.println("Finished!"); 
129  }
130
131  private boolean tx(String[] p) throws IOException {
132    if (p[1].equals("l")) {
133      if (p.length == 4) {
134        return lookup(p[2], p[3]);
135      }
136    } else if (p[1].equals("v")) {
137      if (p.length == 4) {
138        return validate(p[2], p[3], null);
139      } else if (p.length == 5) {
140        return validate(p[2], p[3], p[4]);
141      }
142    }
143    throw new FHIRException("Not understood");
144  }
145
146  private boolean validate(String system, String code, String url) throws IOException {
147    Parameters pin = new Parameters();
148    if (url != null) {
149      pin.addParameter("url", url);
150    }
151    pin.addParameter("system", system);
152    pin.addParameter("code", code);
153    Parameters pout = client.operateType(CodeSystem.class, "validate-code", pin);
154    showParameters("  ", pout.getParameter());
155    return true;
156  }
157
158  private void showParameters(String prefix, List<ParametersParameterComponent> list) {
159    for (ParametersParameterComponent pp : list) {
160      if (pp.hasValue()) {        
161        System.out.println("  "+pp.getName()+": "+pp.getValue().toString());
162      } else if (pp.hasPart()) {
163        showParameters("   ", pp.getPart());
164      }
165    }
166  }
167
168  private boolean lookup(String string, String string2) {
169    throw new FHIRException("Not implemented");
170  }
171
172  private void getImmunizations() throws IOException {
173    Bundle bnd = client.search("Immunization", "?patient="+currentId);
174    System.out.println(""+bnd.getTotal()+" Immunizations found. Printing "+bnd.getEntry().size());
175    
176    for (BundleEntryComponent be : bnd.getEntry()) {
177      Resource imm = be.getResource();
178      System.out.println(summary(imm));
179    } 
180  }
181
182  private void getProcedures() throws IOException {
183
184    Bundle bnd = client.search("Procedure", "?patient="+currentId);
185    System.out.println(""+bnd.getTotal()+" Procedures found. Printing "+bnd.getEntry().size());
186    
187    for (BundleEntryComponent be : bnd.getEntry()) {
188      Resource imm = be.getResource();
189      System.out.println(summary(imm));
190    } 
191  }
192  
193  private void getConditions() throws IOException {
194
195    Bundle bnd = client.search("Condition", "?patient="+currentId);
196    System.out.println(""+bnd.getTotal()+" Conditions found. Printing "+bnd.getEntry().size());
197    
198    for (BundleEntryComponent be : bnd.getEntry()) {
199      Resource imm = be.getResource();
200      System.out.println(summary(imm));
201    } 
202  }
203
204  private void edit(String path, String value) {
205    if (path.contains(".")) {
206      List<Base> list = fpe.evaluate(currentResource, fpe.parse(path.substring(0, path.lastIndexOf("."))));
207      if (list.size() == 1) {
208        path = path.substring(path.lastIndexOf(".")+1);
209        Property p = list.get(0).getNamedProperty(path);
210        Base b = makeValue(p, value);
211        list.get(0).setProperty(path, b);
212      } else {
213        throw new FHIRException("Unable to set value at "+path+": "+list.size()+" matches");
214      }
215    } else {
216      Property p = currentResource.getNamedProperty(path);
217      Base b = makeValue(p, value);
218      currentResource.setProperty(path, b);
219    }
220    currentResource = client.update(currentResource);
221  }
222
223  private Base makeValue(Property p, String value) {
224    switch (p.getTypeCode()) {
225    case "boolean" : return new BooleanType(value);
226    case "code" :  return new CodeType(value);
227    case "string" : return new StringType(value);
228    case "date" : return new DateType(value);
229    }
230    throw new FHIRException("Unhandled type "+p.getTypeCode());
231  }
232
233  private void viewResource(String path) {
234    System.out.println("Current Resource, query = "+path);
235    List<Base> list = fpe.evaluate(currentResource, fpe.parse(path));
236    for (Base b : list) {
237      System.out.println(b.toString());
238    }
239  }
240
241  private void viewResource() throws IOException {
242    System.out.println("Current Resource:");
243    System.out.println(new JsonParser().setOutputStyle(OutputStyle.PRETTY).composeString(currentResource));
244  }
245
246  private void select(String type, String id) throws IOException {
247    if (type.equals("Patient")) {
248      currentResource = client.fetchResource(Patient.class, id);
249    } else if (type.equals("Immunization")) {
250      currentResource = client.fetchResource(Immunization.class, id);
251    } else if (type.equals("Condition")) {
252      currentResource = client.fetchResource(Condition.class, id);
253    } else if (type.equals("Procedure")) {
254      currentResource = client.fetchResource(Procedure.class, id);
255    } else {
256      throw new FHIRException("Unhandled type "+type);
257    }
258    currentId = type+"/"+id;
259    System.out.println("Resource = "+currentId+" "+summary(currentResource));
260  }
261
262  private String summary(Resource r) throws IOException {
263//    return new XmlParser().composeString(r);
264    if (r instanceof Patient) {
265      Patient pat = (Patient) r;
266      return pat.getIdBase()+" "+pat.getGender()+" "+pat.getBirthDateElement().asStringValue()+" "+name(pat);
267    }
268    if (r instanceof Immunization) {
269      Immunization imm = (Immunization) r;
270      return imm.getIdBase()+" "+imm.getOccurrenceDateTimeType().asStringValue()+" "+code(imm.getVaccineCode())+" "+imm.getLotNumber()+" ("+imm.getStatus()+")";
271    }
272    if (r instanceof Condition) {
273      Condition cnd = (Condition) r;
274      return cnd.getIdBase()+" "+code(cnd.getClinicalStatus())+" "+code(cnd.getVerificationStatus())+" "+code(cnd.getCode())+" "+cnd.getRecordedDateElement().asStringValue();
275    }
276    if (r instanceof Procedure) {
277      Procedure prc = (Procedure) r;
278      return prc.getIdBase()+" "+prc.getStatusElement().asStringValue()+" "+code(prc.getCode())+" "+code(prc.getCode())+" "+dt(prc.getPerformed());
279    }
280    return "??";
281  }
282
283  private String dt(Type type) {
284    if (type == null) {
285      return "";
286    }
287    if (type.isPrimitive()) {
288      return type.primitiveValue();
289    }
290    if (type instanceof Period) {
291      Period pd = (Period) type;
292      return pd.getStartElement().asStringValue()+" -> "+pd.getEndElement().asStringValue();
293    }
294    return "??";
295  }
296
297  private String code(CodeableConcept cc) {
298    for (Coding c : cc.getCoding()) {
299      if (c.hasSystem()) {
300        String d = c.hasDisplay() ? " ("+c.getDisplay()+")" : "";
301        if (c.hasCode()) {
302          switch (c.getSystem()) {
303          case "http://hl7.org/fhir/sid/cvx": return "CVX "+c.getCode()+d;
304          case "http://snomed.info/sct": return "SCT "+c.getCode()+d;
305          default:
306            if (Utilities.startsWithInList(c.getSystem(), "http://terminology.hl7.org")) {
307              return c.getCode();
308            } else {
309              throw new FHIRException("Unknown system "+c.getSystem());
310            }
311          }
312        }
313      }
314    }
315    for (Coding c : cc.getCoding()) {
316      if (c.hasCode()) {
317        return c.getCode();
318      }
319    }
320    if (cc.hasText()) {
321      return cc.getText();
322    }
323    return "";
324  }
325
326  private void search(String[] p) throws IOException {
327    if (client == null) {
328      throw new FHIRException("Not connected to to a server");
329    }
330    String search = "?name="+p[1];
331    if (p.length > 2) {
332      search = search +"&gender="+p[2];
333      if (p.length > 3) {
334        search = search +"&birthdate="+p[3];
335      }
336    }
337    Bundle bnd = client.search("Patient", search);
338    System.out.println(""+bnd.getTotal()+" Patients found. Printing "+bnd.getEntry().size());
339    
340    for (BundleEntryComponent be : bnd.getEntry()) {
341      Patient pat = (Patient) be.getResource();
342      System.out.println(summary(pat));
343    } 
344  }
345
346  private String name(Patient pat) {
347    if (pat.getName().size() > 0) {
348      return name(pat.getName().get(0));
349    }
350    return null;
351  }
352
353  private String name(HumanName n) {
354    if (n.hasText()) {
355      return n.getText();
356    }
357    if (n.hasFamily()) {
358      if (n.hasGiven()) {
359        return n.getGiven().get(0)+" "+n.getFamily().toUpperCase();
360      } else {
361        return n.getFamily().toUpperCase();
362      }
363    }
364    return "??";
365  }
366
367  private void connectToServer(String url) throws URISyntaxException {
368    client = new FHIRToolingClient(url, "FHIR-Command-Line-App");
369    CapabilityStatement cs = client.getCapabilitiesStatementQuick();
370    System.out.println("Connected to "+url+": "+cs.getSoftware().getName()+", version "+cs.getFhirVersion().toCode());
371  }
372
373  private void genMenuTx() {
374
375    System.out.println("Simple Client. Commands you can run:");
376    System.out.println(" tx l {system} {code}");
377    System.out.println(" tx v {system} {code} {vs-url}");
378    System.out.println(" ?tx - print this again");    
379    System.out.println(" x - exit");    
380  }
381  
382  private void genMenu() {
383    System.out.println("Simple Client. Commands you can run:");
384    System.out.println(" c {url} - connect to a server");
385    System.out.println(" s {name} [{gender}] {{dob]} - find a patient record");
386    System.out.println(" p {id} - choose a patient record");    
387    System.out.println(" v [{field}] - see a set of field(s) in the current resource, or the whole resource");
388    System.out.println(" e {field} {value} - edit a field in the current resource");
389    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)");
390    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)");
391    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)");
392    System.out.println(" ? - print this again");    
393    System.out.println(" x - exit");    
394  }
395}