001package org.hl7.fhir.convertors.misc;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.FileNotFoundException;
006import java.io.FileOutputStream;
007import java.io.IOException;
008import java.net.URISyntaxException;
009import java.text.ParseException;
010import java.time.Duration;
011import java.util.ArrayList;
012import java.util.Collections;
013import java.util.HashMap;
014import java.util.List;
015import java.util.Map;
016
017import org.hl7.fhir.exceptions.FHIRException;
018import org.hl7.fhir.r4.formats.IParser.OutputStyle;
019import org.hl7.fhir.r4.formats.JsonParser;
020import org.hl7.fhir.r4.model.CapabilityStatement;
021import org.hl7.fhir.r4.model.CodeSystem;
022import org.hl7.fhir.r4.model.IntegerType;
023import org.hl7.fhir.r4.model.OperationOutcome;
024import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
025import org.hl7.fhir.r4.model.OperationOutcome.IssueType;
026import org.hl7.fhir.r4.model.Parameters;
027import org.hl7.fhir.r4.model.UriType;
028import org.hl7.fhir.r4.model.ValueSet;
029import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent;
030import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
031import org.hl7.fhir.r4.terminologies.JurisdictionUtilities;
032import org.hl7.fhir.utilities.CSVReader;
033import org.hl7.fhir.utilities.Utilities;
034import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
035import org.hl7.fhir.utilities.json.model.JsonArray;
036import org.hl7.fhir.utilities.json.model.JsonObject;
037import org.hl7.fhir.utilities.json.model.JsonProperty;
038
039public class VSACImporter extends OIDBasedValueSetImporter {
040  
041  public VSACImporter() throws FHIRException, IOException {
042    super();
043    init();
044  }
045
046  public static void main(String[] args) throws FHIRException, IOException, ParseException, URISyntaxException {
047    VSACImporter self = new VSACImporter();
048    self.process(args[0], args[1], args[2], "true".equals(args[3]), "true".equals(args[4]));
049  }
050
051  private void process(String source, String dest, String apiKey, boolean onlyNew, boolean onlyActive) throws FHIRException, IOException, URISyntaxException {
052    CSVReader csv = new CSVReader(ManagedFileAccess.inStream(source));
053    csv.readHeaders();
054    Map<String, String> errs = new HashMap<>();
055
056    FHIRToolingClient fhirToolingClient = new FHIRToolingClient("https://cts.nlm.nih.gov/fhir", "fhir/vsac");
057    fhirToolingClient.setUsername("apikey");
058    fhirToolingClient.setPassword(apiKey);
059    fhirToolingClient.setTimeoutNormal(30000);
060    fhirToolingClient.setTimeoutExpand(30000);
061
062    CapabilityStatement cs = fhirToolingClient.getCapabilitiesStatement();
063    JsonParser json = new JsonParser();
064    json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path("[tmp]", "vsac-capability-statement.json")), cs);
065
066    System.out.println("CodeSystems");
067    CodeSystem css = fhirToolingClient.fetchResource(CodeSystem.class, "CDCNHSN");
068    json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-CDCNHSN.json")), css);
069    css = fhirToolingClient.fetchResource(CodeSystem.class, "CDCREC");
070    json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-CDCREC.json")), css);
071    css = fhirToolingClient.fetchResource(CodeSystem.class, "HSLOC");
072    json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-HSLOC.json")), css);
073    css = fhirToolingClient.fetchResource(CodeSystem.class, "SOP");
074    json.setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "CodeSystem-SOP.json")), css);
075    
076    System.out.println("Loading");
077    List<String> oids = new ArrayList<>();
078    List<String> allOids = new ArrayList<>();
079    while (csv.line()) {
080      String status = csv.cell("Expansion Status");
081      if (!onlyActive || "Active".equals(status)) {
082        String oid = csv.cell("OID");
083        allOids.add(oid);
084        if (!onlyNew || !(ManagedFileAccess.file(Utilities.path(dest, "ValueSet-" + oid + ".json")).exists())) {
085          oids.add(oid);
086        }
087      }
088    }
089    Collections.sort(oids);
090    System.out.println("Cleaning");
091    cleanValueSets(allOids, dest);
092    System.out.println("Go: "+oids.size()+" oids");
093    int i = 0;
094    int j = 0;
095    long t = System.currentTimeMillis();
096    long tt = System.currentTimeMillis();
097    for (String oid : oids) {
098      try {
099        long t3 = System.currentTimeMillis();
100        if (processOid(dest, onlyNew, errs, fhirToolingClient, oid.trim())) {
101          j++;
102        }
103        i++;
104        System.out.print(":"+((System.currentTimeMillis() - t3) / 1000));
105        if (i % 100 == 0) {
106          long elapsed = System.currentTimeMillis() - t;
107          System.out.println("");
108          System.out.println(i+": "+j+" ("+((j * 100) / i)+"%) @ "+Utilities.describeDuration(elapsed)
109              +", "+(elapsed/100000)+"sec/vs, estimated "+Utilities.describeDuration(estimate(i, oids.size(), tt))+" remaining");
110          t = System.currentTimeMillis();
111        }
112      } catch (Exception e) {
113        e.printStackTrace();
114        System.out.println("Unable to fetch OID " + oid + ": " + e.getMessage());
115        errs.put(oid, e.getMessage());
116      }
117    }
118    
119    OperationOutcome oo = new OperationOutcome();
120    for (String oid : errs.keySet()) {
121      oo.addIssue().setSeverity(IssueSeverity.ERROR).setCode(IssueType.EXCEPTION).setDiagnostics(errs.get(oid)).addLocation(oid);
122    }
123    new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(dest, "other", "OperationOutcome-vsac-errors.json")), oo);
124    System.out.println("Done. " + i + " ValueSets in "+Utilities.describeDuration(System.currentTimeMillis() - tt));
125  }
126
127  private void cleanValueSets(List<String> allOids, String dest) throws IOException {
128    cleanValueSets(allOids, new File(Utilities.path(dest)));
129  }
130
131  private void cleanValueSets(List<String> allOids, File file) {
132    for (File f : file.listFiles()) {
133      if (f.getName().startsWith("ValueSet-")) {
134        String oid = f.getName().substring(9).replace(".json", "");
135        if (!allOids.contains(oid)) {
136          f.delete();
137        }
138      }
139    }
140    
141  }
142
143  private long estimate(int i, int size, long tt) {
144    long elapsed = System.currentTimeMillis() - tt;
145    long average = elapsed / i;
146    return (size - i) * average;
147  }
148
149  private boolean processOid(String dest, boolean onlyNew, Map<String, String> errs, FHIRToolingClient fhirToolingClient, String oid)
150      throws IOException, InterruptedException, FileNotFoundException {
151    
152    while (true) {
153      boolean ok = true;
154      long t = System.currentTimeMillis();
155      ValueSet vs = null;
156      try {
157        vs = fhirToolingClient.read(ValueSet.class, oid);
158      } catch (Exception e) {
159        if (e.getMessage().contains("timed out")) {
160          ok = false;
161        } else {
162          errs.put(oid, "Read: " +e.getMessage());
163          System.out.println("Read "+oid+" failed @ "+Utilities.describeDuration(System.currentTimeMillis()-t)+"ms: "+e.getMessage());
164          return false;
165        }
166      }
167      if (ok) {
168        t = System.currentTimeMillis();
169        try {
170          Parameters p = new Parameters();
171          p.addParameter("url", new UriType(vs.getUrl()));
172          ValueSet vse = fhirToolingClient.expandValueset(null, p);
173          vs.setExpansion(vse.getExpansion());
174        } catch (Exception e) {
175          if (e.getMessage().contains("timed out")) {
176            ok = false;
177          } else {
178            errs.put(oid, "Expansion: " +e.getMessage());
179            System.out.println("Expand "+oid+" failed @ "+Utilities.describeDuration(System.currentTimeMillis()-t)+"ms: "+e.getMessage());
180            return false;
181          }
182        }
183      }
184      if (ok) {
185        while (isIncomplete(vs.getExpansion())) {
186          Parameters p = new Parameters();
187          int offset = vs.getExpansion().getParameter("offset").getValueIntegerType().getValue() + vs.getExpansion().getParameter("count").getValueIntegerType().getValue();
188          p.addParameter("offset", offset);
189          p.addParameter("url", new UriType(vs.getUrl()));
190          t = System.currentTimeMillis();
191          try {
192            ValueSet vse = fhirToolingClient.expandValueset(null, p);    
193            vs.getExpansion().getContains().addAll(vse.getExpansion().getContains());
194            vs.getExpansion().setParameter(vse.getExpansion().getParameter());
195          } catch (Exception e2) {
196            if (e2.getMessage().contains("timed out")) {
197              ok = false;
198              break;
199            } else {
200              errs.put(oid, "Expansion: " +e2.getMessage()+" @ "+offset);
201              System.out.println("Expand "+oid+" @ "+offset+" failed @ "+Utilities.describeDuration(System.currentTimeMillis()-t)+"ms: "+e2.getMessage());
202              return false;
203            }
204          } 
205        }
206      }
207      if (ok) {
208        vs.getExpansion().setOffsetElement(null);
209        vs.getExpansion().getParameter().clear();
210
211        if (vs.hasTitle()) {
212          if (vs.getTitle().equals(vs.getDescription())) {
213            vs.setTitle(vs.getName());              
214          } else {
215            //              System.out.println(oid);
216            //              System.out.println("  name: "+vs.getName());
217            //              System.out.println("  title: "+vs.getTitle());
218            //              System.out.println("  desc: "+vs.getDescription());
219          }
220        } else {
221          vs.setTitle(vs.getName());
222        }
223        if (vs.getUrl().startsWith("https://")) {
224          System.out.println("URL is https: "+vs.getUrl());
225        }
226        vs.setName(makeValidName(vs.getName()));
227        JurisdictionUtilities.setJurisdictionCountry(vs.getJurisdiction(), "US");
228        new JsonParser().setOutputStyle(OutputStyle.NORMAL).compose(ManagedFileAccess.outStream(Utilities.path(dest, "ValueSet-" + oid + ".json")), vs);
229        return true;
230      }
231    }
232  }
233
234  private boolean isIncomplete(ValueSetExpansionComponent expansion) {
235    IntegerType c = expansion.getParameter("count") != null ? expansion.getParameter("count").getValueIntegerType() : new IntegerType(0);
236    IntegerType offset = expansion.getParameter("offset") != null ? expansion.getParameter("offset").getValueIntegerType() : new IntegerType(0);
237    return c.getValue() + offset.getValue() < expansion.getTotal();
238  }
239
240  private String makeValidName(String name) {
241    StringBuilder b = new StringBuilder();
242    boolean upper = true;
243    for (char ch : name.toCharArray()) {
244      if (ch == ' ') {
245        upper = true;
246      } else if (Character.isAlphabetic(ch)) {
247        if (upper) {
248          b.append(Character.toUpperCase(ch));
249        } else {
250          b.append(ch);
251        }
252        upper = false;
253      } else if (Character.isDigit(ch)) {
254        if (b.length() == 0) {
255          b.append('N');
256        }
257        b.append(ch);
258      } else if (ch == '_' && b.length() != 0) {
259        b.append(ch);
260      } else {
261        upper = true;
262      }
263    }
264//    System.out.println(b.toString()+" from "+name);
265    return b.toString();
266  }
267}