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