001package org.hl7.fhir.r5.terminologies;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.util.ArrayList;
035import java.util.Calendar;
036import java.util.Collections;
037import java.util.Comparator;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Set;
041
042import org.hl7.fhir.exceptions.FHIRException;
043import org.hl7.fhir.exceptions.FHIRFormatError;
044import org.hl7.fhir.r5.model.BooleanType;
045import org.hl7.fhir.r5.model.CanonicalResource;
046import org.hl7.fhir.r5.model.CanonicalType;
047import org.hl7.fhir.r5.model.CodeSystem;
048import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
049import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
050import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent;
051import org.hl7.fhir.r5.model.CodeSystem.PropertyType;
052import org.hl7.fhir.r5.model.CodeType;
053import org.hl7.fhir.r5.model.DataType;
054import org.hl7.fhir.r5.model.DateTimeType;
055import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
056import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptDefinitionComponentSorter;
057import org.hl7.fhir.r5.model.Identifier;
058import org.hl7.fhir.r5.model.Meta;
059import org.hl7.fhir.r5.model.UriType;
060import org.hl7.fhir.r5.utils.ToolingExtensions;
061import org.hl7.fhir.utilities.StandardsStatus;
062import org.hl7.fhir.utilities.Utilities;
063
064public class CodeSystemUtilities {
065
066  public static class ConceptDefinitionComponentSorter implements Comparator<ConceptDefinitionComponent> {
067
068    @Override
069    public int compare(ConceptDefinitionComponent o1, ConceptDefinitionComponent o2) {
070      return o1.getCode().compareTo(o2.getCode());
071    }
072
073  }
074
075  public static final String USER_DATA_CROSS_LINK = "cs.utils.cross.link";
076
077  public static class CodeSystemNavigator {
078
079    private CodeSystem cs;
080    private boolean restructure;
081    private Set<String> processed = new HashSet<>();
082
083    public CodeSystemNavigator(CodeSystem cs) {
084      this.cs = cs;
085      restructure = hasExtraRelationships(cs.getConcept());
086    }
087
088    public boolean isRestructure() {
089      return restructure;
090    }
091
092    private boolean hasExtraRelationships(List<ConceptDefinitionComponent> concept) {
093      for (ConceptDefinitionComponent cd : concept) {
094        if (!getSubsumedBy(cd).isEmpty()) {
095          return true;
096        }
097        for (ConceptDefinitionComponent cdc : cd.getConcept()) {
098          if (hasExtraRelationships(cdc.getConcept())) {
099            return true;
100          }
101        }
102      }
103      return false;
104    }
105
106    public List<ConceptDefinitionComponent> getConcepts(ConceptDefinitionComponent context) {
107      if (context == null) {
108        if (restructure) {
109          List<ConceptDefinitionComponent> res = new ArrayList<>();
110          for (ConceptDefinitionComponent cd : cs.getConcept()) {
111            if (getSubsumedBy(cd).isEmpty()) {
112              res.add(cd);
113              processed.add(cd.getCode());
114            }
115          }
116          return res;
117        } else {
118          return cs.getConcept();
119        }
120      } else {
121        if (restructure) {
122          List<ConceptDefinitionComponent> res = new ArrayList<>();
123          for (ConceptDefinitionComponent cd : context.getConcept()) {
124            res.add(cd);
125            processed.add(cd.getCode());
126          }
127          for (ConceptDefinitionComponent cd : cs.getConcept()) {
128            if (getSubsumedBy(cd).contains(context.getCode()) && !processed.contains(cd.getCode())) {
129              res.add(cd);
130              processed.add(cd.getCode());
131            }
132          }
133          return res;
134        } else {
135          return context.getConcept();
136        }
137      }
138    }
139
140    private List<String> getSubsumedBy(ConceptDefinitionComponent cd) {
141      List<String> codes = new ArrayList<>();
142      for (ConceptPropertyComponent cp : cd.getProperty()) {
143        if ("subsumedBy".equals(cp.getCode())) {
144          codes.add(cp.getValue().primitiveValue());
145        }
146      }
147      return codes;
148    }
149
150    public List<ConceptDefinitionComponent> getOtherChildren(ConceptDefinitionComponent context) {
151      List<ConceptDefinitionComponent> res = new ArrayList<>();
152      for (ConceptDefinitionComponent cd : cs.getConcept()) {
153        if (getSubsumedBy(cd).contains(context.getCode()) && processed.contains(cd.getCode())) {
154          res.add(cd);
155        }
156      }
157      return res;
158    }
159  }
160
161
162  public static boolean isNotSelectable(CodeSystem cs, ConceptDefinitionComponent def) {
163    for (ConceptPropertyComponent p : def.getProperty()) {
164      if ("notSelectable".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof BooleanType) 
165        return ((BooleanType) p.getValue()).getValue();
166    }
167    return false;
168  }
169
170  public static void setNotSelectable(CodeSystem cs, ConceptDefinitionComponent concept) throws FHIRFormatError {
171    defineNotSelectableProperty(cs);
172    ConceptPropertyComponent p = getProperty(concept, "notSelectable");
173    if (p != null)
174      p.setValue(new BooleanType(true));
175    else
176      concept.addProperty().setCode("notSelectable").setValue(new BooleanType(true));    
177  }
178
179  public static void defineNotSelectableProperty(CodeSystem cs) {
180    defineCodeSystemProperty(cs, "notSelectable", "Indicates that the code is abstract - only intended to be used as a selector for other concepts", PropertyType.BOOLEAN);
181  }
182
183
184  public enum ConceptStatus {
185    Active, Experimental, Deprecated, Retired;
186
187    public String toCode() {
188      switch (this) {
189      case Active: return "active";
190      case Experimental: return "experimental";
191      case Deprecated: return "deprecated";
192      case Retired: return "retired";
193      default: return null;
194      }
195    }
196  }
197
198  public static void setStatus(CodeSystem cs, ConceptDefinitionComponent concept, ConceptStatus status) throws FHIRFormatError {
199    defineStatusProperty(cs);
200    ConceptPropertyComponent p = getProperty(concept, "status");
201    if (p != null)
202      p.setValue(new CodeType(status.toCode()));
203    else
204      concept.addProperty().setCode("status").setValue(new CodeType(status.toCode()));    
205  }
206
207  public static void defineStatusProperty(CodeSystem cs) {
208    defineCodeSystemProperty(cs, "status", "A property that indicates the status of the concept. One of active, experimental, deprecated, retired", PropertyType.CODE);
209  }
210
211  private static void defineDeprecatedProperty(CodeSystem cs) {
212    defineCodeSystemProperty(cs, "deprecationDate", "The date at which a concept was deprecated. Concepts that are deprecated but not inactive can still be used, but their use is discouraged", PropertyType.DATETIME);
213  }
214
215  public static void defineParentProperty(CodeSystem cs) {
216    defineCodeSystemProperty(cs, "parent", "The concept identified in this property is a parent of the concept on which it is a property. The property type will be 'code'. The meaning of parent/child relationships is defined by the hierarchyMeaning attribute", PropertyType.CODE);
217  }
218
219  public static void defineChildProperty(CodeSystem cs) {
220    defineCodeSystemProperty(cs, "child", "The concept identified in this property is a child of the concept on which it is a property. The property type will be 'code'. The meaning of parent/child relationships is defined by the hierarchyMeaning attribute", PropertyType.CODE);
221  }
222
223  public static boolean isDeprecated(CodeSystem cs, ConceptDefinitionComponent def, boolean ignoreStatus)  {
224    try {
225      for (ConceptPropertyComponent p : def.getProperty()) {
226        if (!ignoreStatus) {
227          if ("status".equals(p.getCode()) && p.hasValue() && p.hasValueCodeType() && "deprecated".equals(p.getValueCodeType().getCode()))
228            return true;
229        }
230        // this, though status should also be set
231        if ("deprecationDate".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof DateTimeType) 
232          return ((DateTimeType) p.getValue()).before(new DateTimeType(Calendar.getInstance()));
233        // legacy  
234        if ("deprecated".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof BooleanType) 
235          return ((BooleanType) p.getValue()).getValue();
236      }
237      return false;
238    } catch (FHIRException e) {
239      return false;
240    }
241  }
242
243  public static void setDeprecated(CodeSystem cs, ConceptDefinitionComponent concept, DateTimeType date) throws FHIRFormatError {
244    setStatus(cs, concept, ConceptStatus.Deprecated);
245    defineDeprecatedProperty(cs);
246    concept.addProperty().setCode("deprecationDate").setValue(date);    
247  }
248  
249  public static boolean isInactive(CodeSystem cs, ConceptDefinitionComponent def) throws FHIRException {
250    for (ConceptPropertyComponent p : def.getProperty()) {
251      if ("status".equals(p.getCode()) && p.hasValueStringType()) 
252        return "inactive".equals(p.getValueStringType().primitiveValue()) || "retired".equals(p.getValueStringType().primitiveValue());
253    }
254    return false;
255  }
256  
257  public static boolean isInactive(CodeSystem cs, String code) throws FHIRException {
258    ConceptDefinitionComponent def = findCode(cs.getConcept(), code);
259    if (def == null)
260      return true;
261    return isInactive(cs, def);
262  }
263
264  public static void defineCodeSystemProperty(CodeSystem cs, String code, String description, PropertyType type) {
265    for (PropertyComponent p : cs.getProperty()) {
266      if (p.getCode().equals(code))
267        return;
268    }
269    cs.addProperty().setCode(code).setDescription(description).setType(type).setUri("http://hl7.org/fhir/concept-properties#"+code);
270  }
271
272  public static String getCodeDefinition(CodeSystem cs, String code) {
273    return getCodeDefinition(cs.getConcept(), code);
274  }
275
276  private static String getCodeDefinition(List<ConceptDefinitionComponent> list, String code) {
277    for (ConceptDefinitionComponent c : list) {
278      if (c.hasCode() &&  c.getCode().equals(code))
279        return c.getDefinition();
280      String s = getCodeDefinition(c.getConcept(), code);
281      if (s != null)
282        return s;
283    }
284    return null;
285  }
286
287  public static CodeSystem makeShareable(CodeSystem cs) {
288    if (!cs.hasExperimental()) {
289      cs.setExperimental(false);
290    }
291
292    if (!cs.hasMeta())
293      cs.setMeta(new Meta());
294    for (UriType t : cs.getMeta().getProfile()) 
295      if ("http://hl7.org/fhir/StructureDefinition/shareablecodesystem".equals(t.getValue()))
296        return cs;
297    cs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablecodesystem"));
298    return cs;
299  }
300
301  public static boolean makeCSShareable(CodeSystem cs) {
302    if (!cs.hasMeta())
303      cs.setMeta(new Meta());
304    for (UriType t : cs.getMeta().getProfile()) 
305      if ("http://hl7.org/fhir/StructureDefinition/shareablecodesystem".equals(t.getValue()))
306        return false;
307    cs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablecodesystem"));
308    return true;
309  }
310
311  public static void setOID(CodeSystem cs, String oid) {
312    if (!oid.startsWith("urn:oid:"))
313       oid = "urn:oid:" + oid;
314    if (!cs.hasIdentifier())
315      cs.addIdentifier(new Identifier().setSystem("urn:ietf:rfc:3986").setValue(oid));
316    else if ("urn:ietf:rfc:3986".equals(cs.getIdentifierFirstRep().getSystem()) && cs.getIdentifierFirstRep().hasValue() && cs.getIdentifierFirstRep().getValue().startsWith("urn:oid:"))
317      cs.getIdentifierFirstRep().setValue(oid);
318    else
319      throw new Error("unable to set OID on code system");
320    
321  }
322
323  public static boolean hasOID(CanonicalResource cs) {
324    return getOID(cs) != null;
325  }
326
327  public static String getOID(CanonicalResource cs) {
328    if (cs.hasIdentifier() && "urn:ietf:rfc:3986".equals(cs.getIdentifierFirstRep().getSystem()) && cs.getIdentifierFirstRep().hasValue() && cs.getIdentifierFirstRep().getValue().startsWith("urn:oid:"))
329        return cs.getIdentifierFirstRep().getValue().substring(8);
330    return null;
331  }
332
333  public static ConceptDefinitionComponent findCode(List<ConceptDefinitionComponent> list, String code) {
334    for (ConceptDefinitionComponent c : list) {
335      if (c.getCode().equals(code))
336        return c;
337      ConceptDefinitionComponent s = findCode(c.getConcept(), code);
338      if (s != null)
339        return s;
340    }
341    return null;
342  }
343
344  public static void markStatus(CodeSystem cs, String wg, StandardsStatus status, String pckage, String fmm, String normativeVersion) throws FHIRException {
345    if (wg != null) {
346      if (!ToolingExtensions.hasExtension(cs, ToolingExtensions.EXT_WORKGROUP) || 
347          (Utilities.existsInList(ToolingExtensions.readStringExtension(cs, ToolingExtensions.EXT_WORKGROUP), "fhir", "vocab") && !Utilities.existsInList(wg, "fhir", "vocab"))) {
348        ToolingExtensions.setCodeExtension(cs, ToolingExtensions.EXT_WORKGROUP, wg);
349      }
350    }
351    if (status != null) {
352      StandardsStatus ss = ToolingExtensions.getStandardsStatus(cs);
353      if (ss == null || ss.isLowerThan(status)) 
354        ToolingExtensions.setStandardsStatus(cs, status, normativeVersion);
355      if (pckage != null) {
356        if (!cs.hasUserData("ballot.package"))
357          cs.setUserData("ballot.package", pckage);
358        else if (!pckage.equals(cs.getUserString("ballot.package")))
359          if (!"infrastructure".equals(cs.getUserString("ballot.package")))
360            System.out.println("Code System "+cs.getUrl()+": ownership clash "+pckage+" vs "+cs.getUserString("ballot.package"));
361      }
362      if (status == StandardsStatus.NORMATIVE) {
363        cs.setExperimental(false);
364        cs.setStatus(PublicationStatus.ACTIVE);
365      }
366    }
367    if (fmm != null) {
368      String sfmm = ToolingExtensions.readStringExtension(cs, ToolingExtensions.EXT_FMM_LEVEL);
369      if (Utilities.noString(sfmm) || Integer.parseInt(sfmm) < Integer.parseInt(fmm)) { 
370        ToolingExtensions.setIntegerExtension(cs, ToolingExtensions.EXT_FMM_LEVEL, Integer.parseInt(fmm));
371      }
372      if (Integer.parseInt(fmm) <= 1) {
373        cs.setExperimental(true);
374      }
375    }
376  }
377
378 
379  public static DataType readProperty(ConceptDefinitionComponent concept, String code) {
380    for (ConceptPropertyComponent p : concept.getProperty())
381      if (p.getCode().equals(code))
382        return p.getValue(); 
383    return null;
384  }
385
386  public static ConceptPropertyComponent getProperty(ConceptDefinitionComponent concept, String code) {
387    for (ConceptPropertyComponent p : concept.getProperty())
388      if (p.getCode().equals(code))
389        return p; 
390    return null;
391  }
392  
393  public static List<ConceptPropertyComponent> getPropertyValues(ConceptDefinitionComponent concept, String code) {
394    List<ConceptPropertyComponent> res = new ArrayList<>();
395    for (ConceptPropertyComponent p : concept.getProperty()) {
396      if (p.getCode().equals(code)) {
397        res.add(p); 
398      }
399    }
400    return res;
401  }
402
403
404  // see http://hl7.org/fhir/R4/codesystem.html#hierachy
405  // returns additional parents not in the heirarchy
406  public static List<String> getOtherChildren(CodeSystem cs, ConceptDefinitionComponent c) {
407    List<String> res = new ArrayList<String>();
408    for (ConceptPropertyComponent p : c.getProperty()) {
409      if ("parent".equals(p.getCode())) {
410        res.add(p.getValue().primitiveValue());
411      }
412    }
413    return res;
414  }
415
416  // see http://hl7.org/fhir/R4/codesystem.html#hierachy
417  public static void addOtherChild(CodeSystem cs, ConceptDefinitionComponent owner, String code) {
418    defineChildProperty(cs);
419    owner.addProperty().setCode("child").setValue(new CodeType(code));
420  }
421
422  public static boolean hasProperty(ConceptDefinitionComponent c, String code) {
423    for (ConceptPropertyComponent cp : c.getProperty()) {
424      if (code.equals(cp.getCode())) {
425        return true;
426      }
427    }
428    return false;
429  }
430
431  public static boolean hasCode(CodeSystem cs, String code) {
432    for (ConceptDefinitionComponent cc : cs.getConcept()) {
433      if (hasCode(cc, code)) {
434        return true;
435      }
436    }
437    return false;
438  }
439
440  private static boolean hasCode(ConceptDefinitionComponent cc, String code) {
441    if (code.equals(cc.getCode())) {
442      return true;
443    }
444    for (ConceptDefinitionComponent c : cc.getConcept()) {
445      if (hasCode(c, code)) {
446        return true;
447      }
448    }
449    return false;
450  }
451
452  public static ConceptDefinitionComponent getCode(CodeSystem cs, String code) {
453    if (code == null) {
454      return null;
455    }
456    for (ConceptDefinitionComponent cc : cs.getConcept()) {
457      ConceptDefinitionComponent cd = getCode(cc, code);
458      if (cd != null) {
459        return cd;
460      }
461    }
462    return null;
463  }
464
465  private static ConceptDefinitionComponent getCode(ConceptDefinitionComponent cc, String code) {
466    if (code.equals(cc.getCode())) {
467      return cc;
468    }
469    for (ConceptDefinitionComponent c : cc.getConcept()) {
470      ConceptDefinitionComponent cd = getCode(c, code);
471      if (cd != null) {
472        return cd;
473      }
474    }
475    return null;
476  }
477
478  public static void crossLinkCodeSystem(CodeSystem cs) {
479    String parent = getPropertyByUrl(cs, "http://hl7.org/fhir/concept-properties#parent");
480    if ((parent != null)) {
481      crossLinkConcepts(cs.getConcept(), cs.getConcept(), parent);
482    }
483  }
484
485  private static String getPropertyByUrl(CodeSystem cs, String url) {
486    for (PropertyComponent pc : cs.getProperty()) {
487      if (url.equals(pc.getUri())) {
488        return pc.getCode();
489      }
490    }
491    return null;
492  }
493
494  private static void crossLinkConcepts(List<ConceptDefinitionComponent> root, List<ConceptDefinitionComponent> focus, String parent) {
495    for (ConceptDefinitionComponent def : focus) {
496      List<ConceptPropertyComponent> pcl = getPropertyValues(def, parent);
497      for (ConceptPropertyComponent pc : pcl) {
498        String code = pc.getValue().primitiveValue();
499        ConceptDefinitionComponent tgt = findCode(root, code);
500        if (!tgt.hasUserData(USER_DATA_CROSS_LINK)) {
501          tgt.setUserData(USER_DATA_CROSS_LINK, new ArrayList<>());
502        }
503        @SuppressWarnings("unchecked")
504        List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) tgt.getUserData(USER_DATA_CROSS_LINK);
505        children.add(def);
506      }      
507      if (def.hasConcept()) {
508        crossLinkConcepts(root, def.getConcept(), parent);
509      }
510    }
511    
512  }
513
514  public static boolean hasHierarchy(CodeSystem cs) {
515    for (ConceptDefinitionComponent c : cs.getConcept()) {
516      if (c.hasConcept()) {
517        return true;
518      }
519    }
520    return false;
521  }
522
523  public static void sortAllCodes(CodeSystem cs) {
524    sortAllCodes(cs.getConcept());
525  }
526
527  private static void sortAllCodes(List<ConceptDefinitionComponent> list) {
528    Collections.sort(list, new ConceptDefinitionComponentSorter());
529    for (ConceptDefinitionComponent cd : list) {
530      if (cd.hasConcept()) {
531        sortAllCodes(cd.getConcept());
532      }
533    }    
534  }
535  
536}