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