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.http.ManagedWebAccess;
036import org.hl7.fhir.utilities.json.model.JsonArray;
037import org.hl7.fhir.utilities.json.model.JsonObject;
038import org.hl7.fhir.utilities.json.model.JsonProperty;
039
040public class VSACImporter extends OIDBasedValueSetImporter {
041  
042  public VSACImporter() throws FHIRException, IOException {
043    super();
044    init();
045  }
046
047  public static void main(String[] args) throws FHIRException, IOException, ParseException, URISyntaxException {
048    VSACImporter self = new VSACImporter();
049    self.process(args[0], args[1], "true".equals(args[2]), "true".equals(args[3]));
050  }
051
052  private void process(String source, String dest, boolean onlyNew, boolean onlyActive) throws FHIRException, IOException, URISyntaxException {
053    CSVReader csv = new CSVReader(ManagedFileAccess.inStream(source));
054    csv.readHeaders();
055    Map<String, String> errs = new HashMap<>();
056
057    ManagedWebAccess.loadFromFHIRSettings();
058    FHIRToolingClient fhirToolingClient = new FHIRToolingClient("https://cts.nlm.nih.gov/fhir", "fhir/vsac");
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();
125    System.out.println("Done. " + i + " ValueSets in "+Utilities.describeDuration(System.currentTimeMillis() - tt));
126  }
127
128  private void cleanValueSets(List<String> allOids, String dest) throws IOException {
129    cleanValueSets(allOids, ManagedFileAccess.file(Utilities.path(dest)));
130  }
131
132  private void cleanValueSets(List<String> allOids, File file) {
133    for (File f : file.listFiles()) {
134      if (f.getName().startsWith("ValueSet-")) {
135        String oid = f.getName().substring(9).replace(".json", "");
136        if (!allOids.contains(oid)) {
137          f.delete();
138        }
139      }
140    }
141    
142  }
143
144  private long estimate(int i, int size, long tt) {
145    long elapsed = System.currentTimeMillis() - tt;
146    long average = elapsed / i;
147    return (size - i) * average;
148  }
149
150  private boolean processOid(String dest, boolean onlyNew, Map<String, String> errs, FHIRToolingClient fhirToolingClient, String oid)
151      throws IOException, InterruptedException, FileNotFoundException {
152    
153    while (true) {
154      boolean ok = true;
155      long t = System.currentTimeMillis();
156      ValueSet vs = null;
157      try {
158        vs = fhirToolingClient.read(ValueSet.class, oid);
159      } catch (Exception e) {
160        if (e.getMessage().contains("timed out")) {
161          ok = false;
162        } else {
163          errs.put(oid, "Read: " +e.getMessage());
164          System.out.println("Read "+oid+" failed @ "+Utilities.describeDuration(System.currentTimeMillis()-t)+"ms: "+e.getMessage());
165          return false;
166        }
167      }
168      if (ok) {
169        t = System.currentTimeMillis();
170        try {
171          Parameters p = new Parameters();
172          p.addParameter("url", new UriType(vs.getUrl()));
173          ValueSet vse = fhirToolingClient.expandValueset(null, p);
174          vs.setExpansion(vse.getExpansion());
175        } catch (Exception e) {
176          if (e.getMessage().contains("timed out")) {
177            ok = false;
178          } else {
179            errs.put(oid, "Expansion: " +e.getMessage());
180            System.out.println("Expand "+oid+" failed @ "+Utilities.describeDuration(System.currentTimeMillis()-t)+"ms: "+e.getMessage());
181            return false;
182          }
183        }
184      }
185      if (ok) {
186        while (isIncomplete(vs.getExpansion())) {
187          Parameters p = new Parameters();
188          int offset = vs.getExpansion().getParameter("offset").getValueIntegerType().getValue() + vs.getExpansion().getParameter("count").getValueIntegerType().getValue();
189          p.addParameter("offset", offset);
190          p.addParameter("url", new UriType(vs.getUrl()));
191          t = System.currentTimeMillis();
192          try {
193            ValueSet vse = fhirToolingClient.expandValueset(null, p);    
194            vs.getExpansion().getContains().addAll(vse.getExpansion().getContains());
195            vs.getExpansion().setParameter(vse.getExpansion().getParameter());
196          } catch (Exception e2) {
197            if (e2.getMessage().contains("timed out")) {
198              ok = false;
199              break;
200            } else {
201              errs.put(oid, "Expansion: " +e2.getMessage()+" @ "+offset);
202              System.out.println("Expand "+oid+" @ "+offset+" failed @ "+Utilities.describeDuration(System.currentTimeMillis()-t)+"ms: "+e2.getMessage());
203              return false;
204            }
205          } 
206        }
207      }
208      if (ok) {
209        vs.getExpansion().setOffsetElement(null);
210        vs.getExpansion().getParameter().clear();
211
212        if (vs.hasTitle()) {
213          if (vs.getTitle().equals(vs.getDescription())) {
214            vs.setTitle(vs.getName());              
215          } else {
216            //              System.out.println(oid);
217            //              System.out.println("  name: "+vs.getName());
218            //              System.out.println("  title: "+vs.getTitle());
219            //              System.out.println("  desc: "+vs.getDescription());
220          }
221        } else {
222          vs.setTitle(vs.getName());
223        }
224        if (vs.getUrl().startsWith("https://")) {
225          System.out.println("URL is https: "+vs.getUrl());
226        }
227        vs.setName(makeValidName(vs.getName()));
228        JurisdictionUtilities.setJurisdictionCountry(vs.getJurisdiction(), "US");
229        new JsonParser().setOutputStyle(OutputStyle.NORMAL).compose(ManagedFileAccess.outStream(Utilities.path(dest, "ValueSet-" + oid + ".json")), vs);
230        return true;
231      }
232    }
233  }
234
235  private boolean isIncomplete(ValueSetExpansionComponent expansion) {
236    IntegerType c = expansion.getParameter("count") != null ? expansion.getParameter("count").getValueIntegerType() : new IntegerType(0);
237    IntegerType offset = expansion.getParameter("offset") != null ? expansion.getParameter("offset").getValueIntegerType() : new IntegerType(0);
238    return c.getValue() + offset.getValue() < expansion.getTotal();
239  }
240
241  private String makeValidName(String name) {
242    StringBuilder b = new StringBuilder();
243    boolean upper = true;
244    for (char ch : name.toCharArray()) {
245      if (ch == ' ') {
246        upper = true;
247      } else if (Character.isAlphabetic(ch)) {
248        if (upper) {
249          b.append(Character.toUpperCase(ch));
250        } else {
251          b.append(ch);
252        }
253        upper = false;
254      } else if (Character.isDigit(ch)) {
255        if (b.length() == 0) {
256          b.append('N');
257        }
258        b.append(ch);
259      } else if (ch == '_' && b.length() != 0) {
260        b.append(ch);
261      } else {
262        upper = true;
263      }
264    }
265//    System.out.println(b.toString()+" from "+name);
266    return b.toString();
267  }
268}