001package org.hl7.fhir.convertors.misc;
002
003import com.google.gson.JsonElement;
004import com.google.gson.JsonObject;
005import org.hl7.fhir.r4.formats.IParser.OutputStyle;
006import org.hl7.fhir.r4.formats.XmlParser;
007import org.hl7.fhir.r4.formats.XmlParserBase.XmlVersion;
008import org.hl7.fhir.r4.model.*;
009import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
010import org.hl7.fhir.r4.model.CodeSystem.CodeSystemHierarchyMeaning;
011import org.hl7.fhir.r4.model.CodeSystem.ConceptPropertyComponent;
012import org.hl7.fhir.r4.model.CodeSystem.PropertyType;
013import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
014import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
015import org.hl7.fhir.r4.model.ValueSet.FilterOperator;
016import org.hl7.fhir.r4.terminologies.CodeSystemUtilities;
017import org.hl7.fhir.r4.utils.ToolingExtensions;
018import org.hl7.fhir.utilities.TextFile;
019import org.hl7.fhir.utilities.Utilities;
020import org.hl7.fhir.utilities.SimpleHTTPClient;
021import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult;
022import org.hl7.fhir.utilities.json.JsonTrackingParser;
023
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.net.URL;
027import java.util.Date;
028import java.util.HashSet;
029import java.util.Set;
030
031public class ICD11Generator {
032
033  public static void main(String[] args) throws IOException {
034    new ICD11Generator().execute(args[0], args[1]);
035  }
036
037  private void execute(String base, String dest) throws IOException {
038    CodeSystem cs = makeMMSCodeSystem();
039    JsonObject version = fetchJson(Utilities.pathURL(base, "/icd/release/11/mms"));
040    String[] p = version.get("latestRelease").getAsString().split("\\/");
041    cs.setVersion(p[6]);
042    JsonObject root = fetchJson(url(base, version.get("latestRelease").getAsString()));
043    cs.setDateElement(new DateTimeType(root.get("releaseDate").getAsString()));
044    for (JsonElement child : root.getAsJsonArray("child")) {
045      processMMSEntity(cs, base, child.getAsString(), cs.addConcept(), dest);
046      System.out.println();
047    }
048    new XmlParser(XmlVersion.V1_1).setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path(dest, "icd-11-mms.xml")), cs);
049    makeFullVs(dest, cs);
050
051    cs = makeEntityCodeSystem();
052    root = fetchJson(Utilities.pathURL(base, "/icd/entity"));
053    cs.setVersion(root.get("releaseId").getAsString());
054    cs.setDateElement(new DateTimeType(root.get("releaseDate").getAsString()));
055    cs.setTitle(readString(root, "title"));
056    Set<String> ids = new HashSet<>();
057    for (JsonElement child : root.getAsJsonArray("child")) {
058      processEntity(cs, ids, base, tail(child.getAsString()), dest);
059      System.out.println();
060    }
061    new XmlParser(XmlVersion.V1_1).setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path(dest, "icd-11-foundation.xml")), cs);
062    makeFullVs2(dest, cs);
063    System.out.println("finished");
064  }
065
066  private void processEntity(CodeSystem cs, Set<String> ids, String base, String id, String dest) throws IOException {
067    if (!ids.contains(id)) {
068      System.out.print(".");
069      ids.add(id);
070      org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc = cs.addConcept().setCode(id);
071      JsonObject entity = fetchJson(Utilities.pathURL(base, "icd", "entity", id));
072      cc.setDisplay(readString(entity, "title"));
073      String d = readString(entity, "definition");
074      if (d != null) {
075        cc.setDefinition(d);
076      }
077      if (entity.has("inclusion")) {
078        for (JsonElement child : entity.getAsJsonArray("inclusion")) {
079          JsonObject co = (JsonObject) child;
080          String v = readString(co, "label");
081          if (v != null) {
082            if (co.has("foundationReference")) {
083              cc.addProperty().setValue(new Coding().setSystem("http://id.who.int/icd11/foundation").setCode(tail(co.get("foundationReference").getAsString())).setDisplay(v)).setCode("inclusion");
084            }
085          }
086        }
087      }
088      if (entity.has("exclusion")) {
089        for (JsonElement child : entity.getAsJsonArray("exclusion")) {
090          JsonObject co = (JsonObject) child;
091          String v = readString(co, "label");
092          if (v != null) {
093            if (co.has("foundationReference")) {
094              cc.addProperty().setValue(new Coding().setSystem("http://id.who.int/icd11/foundation").setCode(tail(co.get("foundationReference").getAsString())).setDisplay(v)).setCode("exclusion");
095            }
096          }
097        }
098      }
099      if (entity.has("narrowerTerm")) {
100        for (JsonElement child : entity.getAsJsonArray("narrowerTerm")) {
101          JsonObject co = (JsonObject) child;
102          String v = readString(co, "label");
103          if (v != null) {
104            if (co.has("narrowerTerm")) {
105              cc.addProperty().setValue(new Coding().setSystem("http://id.who.int/icd11/foundation").setCode(tail(co.get("foundationReference").getAsString())).setDisplay(v)).setCode("narrowerTerm");
106            }
107          }
108        }
109      }
110      addDesignation(readString(entity, "longDefinition"), cc, "http://id.who.int/icd11/mms/designation", "longDefinition");
111      addDesignation(readString(entity, "fullySpecifiedName"), cc, "http://snomed.info/sct", "900000000000003001");
112      if (entity.has("synonym")) {
113        for (JsonElement j : entity.getAsJsonArray("synonym")) {
114          String v = readString((JsonObject) j, "label");
115          if (v != null && !v.equals(cc.getDisplay())) {
116            addDesignation(v, cc, "http://id.who.int/icd11/mms/designation", "synonym");
117          }
118        }
119      }
120      for (JsonElement j : entity.getAsJsonArray("parent")) {
121        String v = j.getAsString();
122        if (!"http://id.who.int/icd/entity".equals(v)) {
123          cc.addProperty().setValue(new CodeType(tail(v))).setCode("narrowerTerm");
124        }
125      }
126      if (entity.has("child")) {
127        for (JsonElement j : entity.getAsJsonArray("child")) {
128          String v = j.getAsString();
129          cc.addProperty().setValue(new CodeType(tail(v))).setCode("child");
130          processEntity(cs, ids, base, tail(v), dest);
131        }
132      }
133    }
134  }
135
136  private void makeFullVs(String dest, CodeSystem cs) throws IOException {
137    String url = "http://id.who.int/icd11/ValueSet/all-MMS";
138    ValueSet vs = new ValueSet();
139    vs.setId("all-MMS");
140    vs.setUrl(url);
141    vs.setName("ICDMMSAll");
142    vs.setTitle("Value Set for all ICD MMS Codes");
143    vs.setStatus(PublicationStatus.ACTIVE);
144    vs.setExperimental(false);
145    vs.setDate(cs.getDate());
146    vs.setPublisher("WHO");
147    vs.setCopyright("Consult WHO For terms of use");
148    vs.setVersion(cs.getVersion());
149    vs.setStatus(cs.getStatus());
150    ConceptSetComponent inc = vs.getCompose().addInclude();
151    inc.setSystem(cs.getUrl());
152    new XmlParser(XmlVersion.V1_1).setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path(dest, "vs-all-MMS.xml")), vs);
153  }
154
155  private void makeFullVs2(String dest, CodeSystem cs) throws IOException {
156    String url = "http://id.who.int/icd11/ValueSet/all-foundation";
157    ValueSet vs = new ValueSet();
158    vs.setId("all-foundation");
159    vs.setUrl(url);
160    vs.setName("ICDFoundationAll");
161    vs.setTitle("Value Set for all ICD Foundation Concepts");
162    vs.setStatus(PublicationStatus.ACTIVE);
163    vs.setExperimental(false);
164    vs.setDate(cs.getDate());
165    vs.setPublisher("WHO");
166    vs.setCopyright("Consult WHO For terms of use");
167    vs.setVersion(cs.getVersion());
168    vs.setStatus(cs.getStatus());
169    ConceptSetComponent inc = vs.getCompose().addInclude();
170    inc.setSystem(cs.getUrl());
171    new XmlParser(XmlVersion.V1_1).setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path(dest, "vs-all-foundation.xml")), vs);
172  }
173
174  private void processMMSEntity(CodeSystem cs, String base, String ref, org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc, String dest) throws IOException {
175    System.out.print(".");
176    JsonObject entity = fetchJson(url(base, ref));
177    cc.setId(tail(ref));
178    if (entity.has("code") && !Utilities.noString(entity.get("code").getAsString())) {
179      cc.setCode(entity.get("code").getAsString());
180    } else if (entity.has("blockId") && !Utilities.noString(entity.get("blockId").getAsString())) {
181      cc.setCode(entity.get("blockId").getAsString());
182    } else {
183      cc.setCode(cc.getId());
184      cc.addProperty().setCode("abstract").setValue(new BooleanType(true));
185    }
186    if (entity.has("classKind") && !Utilities.noString(entity.get("classKind").getAsString()) && !"category".equals(entity.get("classKind").getAsString())) {
187      cc.addProperty().setCode("kind").setValue(new CodeType(entity.get("classKind").getAsString()));
188    }
189    cc.setDisplay(readString(entity, "title"));
190    StringBuilder defn = new StringBuilder();
191    String d = readString(entity, "definition");
192    if (d != null) {
193      defn.append(d);
194    }
195    if (d == null && (entity.has("inclusion") || entity.has("exclusion"))) {
196      defn.append(cc.getDisplay());
197    }
198    if (entity.has("inclusion")) {
199      defn.append(". Includes: ");
200      boolean first = true;
201      for (JsonElement child : entity.getAsJsonArray("inclusion")) {
202        if (first) first = false;
203        else defn.append(", ");
204        defn.append(readString((JsonObject) child, "label"));
205      }
206    }
207    if (entity.has("exclusion")) {
208      defn.append(". Excludes: ");
209      boolean first = true;
210      for (JsonElement child : entity.getAsJsonArray("exclusion")) {
211        if (first) first = false;
212        else defn.append(", ");
213        JsonObject co = (JsonObject) child;
214        String v = readString(co, "label");
215        if (v != null) {
216          defn.append(v);
217          if (co.has("linearizationReference")) {
218            cc.addProperty().setValue(new Coding().setSystem("http://id.who.int/icd11/mms").setCode(tail(co.get("linearizationReference").getAsString())).setDisplay(v)).setCode("exclusion");
219          }
220        }
221      }
222    }
223    cc.setDefinition(defn.toString());
224    addDesignation(readString(entity, "longDefinition"), cc, "http://id.who.int/icd11/mms/designation", "longDefinition");
225    addDesignation(readString(entity, "fullySpecifiedName"), cc, "http://snomed.info/sct", "900000000000003001");
226    addProperty(readString(entity, "codingNote"), cc, "codingNote");
227    if (entity.has("indexTerm")) {
228//      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("; ");
229//      for (JsonElement child : entity.getAsJsonArray("indexTerm")) {
230//        processIndexTerm(cc, b, (JsonObject) child);    
231//      }
232//      if (b.length() > 0) {
233//        cc.addProperty().setCode("terms").setValue(new StringType(b.toString()));        
234//      }
235      for (JsonElement child : entity.getAsJsonArray("indexTerm")) {
236        processIndexTerm(cc, (JsonObject) child);
237      }
238    }
239    if (entity.has("postcoordinationScale")) {
240      for (JsonElement child : entity.getAsJsonArray("postcoordinationScale")) {
241        JsonObject o = (JsonObject) child;
242        String name = tail(o.get("axisName").getAsString());
243        ConceptPropertyComponent prop = cc.addProperty();
244        prop.setCode("postcoordinationScale");
245        prop.setValue(new CodeType(name));
246        ToolingExtensions.addBooleanExtension(prop, "http://id.who.int/icd11/extensions/required", o.get("requiredPostcoordination").getAsBoolean());
247        ToolingExtensions.addBooleanExtension(prop, "http://id.who.int/icd11/extensions/repeats", o.get("allowMultipleValues").getAsBoolean());
248        if (o.has("scaleEntity")) {
249          ToolingExtensions.addUriExtension(prop, "http://id.who.int/icd11/extensions/valueSet", buildValueSet(cs, cc.getCode(), name, o, dest));
250        }
251      }
252    }
253    if (entity.has("child")) {
254      for (JsonElement child : entity.getAsJsonArray("child")) {
255        processMMSEntity(cs, base, child.getAsString(), cc.addConcept(), dest);
256      }
257    }
258  }
259
260  private String buildValueSet(CodeSystem cs, String code, String name, JsonObject o, String dest) throws IOException {
261    String id = code + "-" + name;
262    String url = "http://id.who.int/icd11/ValueSet/" + id;
263    ValueSet vs = new ValueSet();
264    vs.setId(id);
265    vs.setUrl(url);
266    vs.setName("VS" + name + "4" + code);
267    vs.setTitle("Value Set for " + name + " on " + code);
268    vs.setStatus(PublicationStatus.ACTIVE);
269    vs.setExperimental(false);
270    vs.setDate(cs.getDate());
271    vs.setPublisher("WHO");
272    vs.setCopyright("Consult WHO For terms of use");
273    vs.setVersion(cs.getVersion());
274    vs.setStatus(cs.getStatus());
275    ConceptSetComponent inc = vs.getCompose().addInclude();
276    inc.setSystem(cs.getUrl());
277    for (JsonElement e : o.getAsJsonArray("scaleEntity")) {
278      inc.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue(tail(e.getAsString()));
279    }
280    new XmlParser(XmlVersion.V1_1).setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path(dest, "vs-" + id + ".xml")), vs);
281    return url;
282  }
283
284  private void processIndexTerm(org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc, JsonObject child) {
285    String s = readString(child, "label");
286    if (s != null) {
287      if (!s.equals(cc.getDisplay())) {
288        cc.addDesignation().setValue(s).setUse(new Coding().setSystem("http://id.who.int/icd11/mms/designation").setCode("term"));
289      }
290    }
291  }
292
293  //  private void processIndexTerm(org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc, CommaSeparatedStringBuilder b, JsonObject child) {
294//    String s = readString(child, "label");
295//    if (s != null) {
296//      if (!s.equals(cc.getDisplay())) {
297//        b.append(s);        
298//      }
299//    }
300//    
301//  }
302//
303  private String tail(String ref) {
304    return ref.substring(ref.lastIndexOf("/") + 1);
305  }
306
307  private void addExtension(String v, org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc, String url) {
308    if (v != null) {
309      ToolingExtensions.setStringExtension(cc, url, v);
310    }
311  }
312
313  private void addDesignation(String v, org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc, String system, String code) {
314    if (v != null) {
315      cc.addDesignation().setValue(v.replace("\r", "").replace("\n", "")).setUse(new Coding().setSystem(system).setCode(code));
316    }
317  }
318
319  private void addProperty(String v, org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent cc, String code) {
320    if (v != null) {
321      cc.addProperty().setValue(new StringType(v.replace("\r", " ").replace("\n", ""))).setCode(code);
322    }
323  }
324
325  private String readString(JsonObject obj, String name) {
326    JsonObject p = obj.getAsJsonObject(name);
327    if (p == null) {
328      return null;
329    }
330    if (p.has("@value")) {
331      return p.get("@value").getAsString();
332    }
333    return null;
334  }
335
336  private String url(String base, String u) {
337    return u.replace("http://id.who.int", base);
338  }
339
340  private CodeSystem makeMMSCodeSystem() {
341    CodeSystem cs = new CodeSystem();
342    cs.setId("icd11-mms");
343    cs.setUrl("http://id.who.int/icd11/mms");
344    cs.setName("ICD11MMS");
345    cs.setTitle("ICD-11 MMS Linearization");
346    cs.setStatus(PublicationStatus.ACTIVE);
347    cs.setExperimental(false);
348    cs.setDate(new Date());
349    cs.setPublisher("WHO");
350    cs.setCopyright("Consult WHO For terms of use");
351    cs.setCaseSensitive(true);
352    cs.setHierarchyMeaning(CodeSystemHierarchyMeaning.CLASSIFIEDWITH);
353    cs.setCompositional(true);
354    cs.setVersionNeeded(true);
355    cs.setValueSet("http://id.who.int/icd11/ValueSet/all-MMS");
356    cs.setContent(CodeSystemContentMode.COMPLETE);
357    CodeSystemUtilities.defineCodeSystemProperty(cs, "kind", "The kind of artifact this concept represents", PropertyType.CODE).setUri("http://id.who.int/icd11/properties#kind");
358    CodeSystemUtilities.defineCodeSystemProperty(cs, "terms", "Other keywords for searching", PropertyType.STRING).setUri("http://id.who.int/icd11/properties#terms");
359    CodeSystemUtilities.defineCodeSystemProperty(cs, "codingNote", "Coding advice for this concept", PropertyType.STRING).setUri("http://id.who.int/icd11/properties#codingNote");
360    CodeSystemUtilities.defineCodeSystemProperty(cs, "exclusion", "References to diseases that are excluded from this concept", PropertyType.CODING).setUri("http://id.who.int/icd11/properties#exclusion");
361    CodeSystemUtilities.defineCodeSystemProperty(cs, "abstract", "If concept is abstract", PropertyType.BOOLEAN);
362    CodeSystemUtilities.defineCodeSystemProperty(cs, "postcoordinationScale", "", PropertyType.CODE).setUri("http://id.who.int/icd11/properties#postcoordinationScale");
363    return cs;
364  }
365
366  private CodeSystem makeEntityCodeSystem() {
367    CodeSystem cs = new CodeSystem();
368    cs.setId("icd11-foundation");
369    cs.setUrl("http://id.who.int/icd11/foundation");
370    cs.setName("ICD11Entity");
371    cs.setTitle("ICD-11 Entities (Foundation)");
372    cs.setStatus(PublicationStatus.ACTIVE);
373    cs.setExperimental(false);
374    cs.setDate(new Date());
375    cs.setPublisher("WHO");
376    cs.setCopyright("Consult WHO For terms of use");
377    cs.setCaseSensitive(true);
378    cs.setHierarchyMeaning(CodeSystemHierarchyMeaning.ISA); // though we aren't going to have a heirarchy
379//    cs.setCompositional(true);
380//    cs.setVersionNeeded(true);
381    cs.setValueSet("http://id.who.int/icd11/ValueSet/all-foundation");
382    cs.setContent(CodeSystemContentMode.COMPLETE);
383    CodeSystemUtilities.defineCodeSystemProperty(cs, "exclusion", "References to diseases that are excluded from this concept", PropertyType.CODING).setUri("http://id.who.int/icd11/properties#exclusion");
384    CodeSystemUtilities.defineCodeSystemProperty(cs, "inclusion", "References to diseases that are included from this concept", PropertyType.CODING).setUri("http://id.who.int/icd11/properties#inclusion");
385    CodeSystemUtilities.defineCodeSystemProperty(cs, "narrowerTerm", "Narrower terms for this entity", PropertyType.CODE).setUri("http://id.who.int/icd11/properties#narrowerTerm");
386    CodeSystemUtilities.defineCodeSystemProperty(cs, "parent", "Parent for this concept", PropertyType.CODE);
387    CodeSystemUtilities.defineCodeSystemProperty(cs, "child", "Child for this concept", PropertyType.CODE);
388    return cs;
389  }
390
391
392  private JsonObject fetchJson(String source) throws IOException {
393    SimpleHTTPClient http = new SimpleHTTPClient();
394    http.addHeader("API-Version", "v2");
395    http.addHeader("Accept-Language", "en");
396    HTTPResult res = http.get(source, "application/json");
397    res.checkThrowException();
398    return JsonTrackingParser.parseJson(res.getContent());
399  }
400
401
402}