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