001package org.hl7.fhir.r5.terminologies;
002
003import java.util.Calendar;
004import java.util.Collections;
005import java.util.Comparator;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Set;
009
010/*
011  Copyright (c) 2011+, HL7, Inc.
012  All rights reserved.
013  
014  Redistribution and use in source and binary forms, with or without modification, 
015  are permitted provided that the following conditions are met:
016    
017   * Redistributions of source code must retain the above copyright notice, this 
018     list of conditions and the following disclaimer.
019   * Redistributions in binary form must reproduce the above copyright notice, 
020     this list of conditions and the following disclaimer in the documentation 
021     and/or other materials provided with the distribution.
022   * Neither the name of HL7 nor the names of its contributors may be used to 
023     endorse or promote products derived from this software without specific 
024     prior written permission.
025  
026  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
027  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
028  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
029  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
030  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
031  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
032  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
033  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
034  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
035  POSSIBILITY OF SUCH DAMAGE.
036  
037 */
038
039
040import org.hl7.fhir.exceptions.FHIRException;
041import org.hl7.fhir.exceptions.FHIRFormatError;
042import org.hl7.fhir.r5.context.IWorkerContext;
043import org.hl7.fhir.r5.model.BooleanType;
044import org.hl7.fhir.r5.model.CanonicalType;
045import org.hl7.fhir.r5.model.CodeSystem;
046import org.hl7.fhir.r5.model.DateTimeType;
047import org.hl7.fhir.r5.model.StringType;
048import org.hl7.fhir.r5.model.IntegerType;
049import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
050import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
051import org.hl7.fhir.r5.model.Identifier;
052import org.hl7.fhir.r5.model.Meta;
053import org.hl7.fhir.r5.model.UriType;
054import org.hl7.fhir.r5.model.ValueSet;
055import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
056import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
057import org.hl7.fhir.r5.model.CodeType;
058import org.hl7.fhir.r5.model.Coding;
059import org.hl7.fhir.r5.model.DataType;
060import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
061import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
062import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent;
063import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent;
064import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
065import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent;
066import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptDefinitionComponentSorter;
067import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptStatus;
068import org.hl7.fhir.r5.utils.CanonicalResourceUtilities;
069import org.hl7.fhir.r5.utils.ToolingExtensions;
070import org.hl7.fhir.utilities.StandardsStatus;
071import org.hl7.fhir.utilities.Utilities;
072import org.hl7.fhir.utilities.VersionUtilities;
073
074public class ValueSetUtilities extends TerminologyUtilities {
075
076
077  public static class ValueSetSorter implements Comparator<ValueSet> {
078
079    @Override
080    public int compare(ValueSet o1, ValueSet o2) {
081      String url1 = o1.getUrl();
082      String url2 = o2.getUrl();
083      int c = compareString(url1, url2);
084      if (c == 0) {
085        String ver1 = o1.getVersion();
086        String ver2 = o2.getVersion();
087        c = VersionUtilities.compareVersions(ver1, ver2);
088        if (c == 0) {
089          String d1 = o1.getDateElement().asStringValue();
090          String d2 = o2.getDateElement().asStringValue();
091          c = compareString(url1, url2);
092        }
093      }
094      return c;
095    }
096
097    private int compareString(String s1, String s2) {
098      if (s1 == null) {
099        return s2 == null ? 0 : 1;
100      } else {
101        return s1.compareTo(s2);
102      }
103    }
104
105  }
106
107
108  public static boolean isServerSide(String url) {
109    return Utilities.existsInList(url, "http://hl7.org/fhir/sid/cvx");
110  }
111  
112  public static ValueSet makeShareable(ValueSet vs) {
113    if (!vs.hasExperimental()) {
114      vs.setExperimental(false);
115    }
116    if (!vs.hasMeta())
117      vs.setMeta(new Meta());
118    for (UriType t : vs.getMeta().getProfile()) 
119      if (t.getValue().equals("http://hl7.org/fhir/StructureDefinition/shareablevalueset"))
120        return vs;
121    vs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablevalueset"));
122    return vs;
123  }
124
125  public static boolean makeVSShareable(ValueSet vs) {
126    if (!vs.hasMeta())
127      vs.setMeta(new Meta());
128    for (UriType t : vs.getMeta().getProfile()) 
129      if (t.getValue().equals("http://hl7.org/fhir/StructureDefinition/shareablevalueset"))
130        return false;
131    vs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablevalueset"));
132    return true;
133  }
134
135  public static void checkShareable(ValueSet vs) {
136    if (!vs.hasMeta())
137      throw new Error("ValueSet "+vs.getUrl()+" is not shareable");
138    for (UriType t : vs.getMeta().getProfile()) {
139      if (t.getValue().equals("http://hl7.org/fhir/StructureDefinition/shareablevalueset"))
140        return;
141    }
142    throw new Error("ValueSet "+vs.getUrl()+" is not shareable");    
143  }
144
145  public static boolean hasOID(ValueSet vs) {
146    return getOID(vs) != null;
147  }
148
149  public static String getOID(ValueSet vs) {
150    for (Identifier id : vs.getIdentifier()) {
151      if ("urn:ietf:rfc:3986".equals(id.getSystem()) && id.hasValue() && id.getValue().startsWith("urn:oid:"))
152        return id.getValue().substring(8);
153    }
154    return null;
155  }
156
157  public static void setOID(ValueSet vs, String oid) {
158    if (!oid.startsWith("urn:oid:"))
159      oid = "urn:oid:" + oid;
160    for (Identifier id : vs.getIdentifier()) {
161      if ("urn:ietf:rfc:3986".equals(id.getSystem()) && id.hasValue() && id.getValue().startsWith("urn:oid:")) {
162        id.setValue(oid);
163        return;
164      }
165    }
166    vs.addIdentifier().setSystem("urn:ietf:rfc:3986").setValue(oid);
167  }
168
169  public static void markStatus(ValueSet vs, String wg, StandardsStatus status, String pckage, String fmm, IWorkerContext context, String normativeVersion) throws FHIRException {
170    if (vs.hasUserData("external.url"))
171      return;
172    
173    if (wg != null) {
174      if (!ToolingExtensions.hasExtension(vs, ToolingExtensions.EXT_WORKGROUP) || 
175          (!Utilities.existsInList(ToolingExtensions.readStringExtension(vs, ToolingExtensions.EXT_WORKGROUP), "fhir", "vocab") && Utilities.existsInList(wg, "fhir", "vocab"))) {
176        CanonicalResourceUtilities.setHl7WG(vs, wg);
177      }
178    }
179    if (status != null) {
180      StandardsStatus ss = ToolingExtensions.getStandardsStatus(vs);
181      if (ss == null || ss.isLowerThan(status)) 
182        ToolingExtensions.setStandardsStatus(vs, status, normativeVersion);
183      if (pckage != null) {
184        if (!vs.hasUserData("ballot.package"))        
185          vs.setUserData("ballot.package", pckage);
186        else if (!pckage.equals(vs.getUserString("ballot.package")))
187          if (!"infrastructure".equals(vs.getUserString("ballot.package")))
188          System.out.println("Value Set "+vs.getUrl()+": ownership clash "+pckage+" vs "+vs.getUserString("ballot.package"));
189      }
190      if (status == StandardsStatus.NORMATIVE) {
191        vs.setExperimental(false);
192        vs.setStatus(PublicationStatus.ACTIVE);
193      }
194    }
195    if (fmm != null) {
196      String sfmm = ToolingExtensions.readStringExtension(vs, ToolingExtensions.EXT_FMM_LEVEL);
197      if (Utilities.noString(sfmm) || Integer.parseInt(sfmm) < Integer.parseInt(fmm))  {
198        ToolingExtensions.setIntegerExtension(vs, ToolingExtensions.EXT_FMM_LEVEL, Integer.parseInt(fmm));
199      }
200      if (Integer.parseInt(fmm) <= 1) {
201        vs.setExperimental(true);
202      }
203    }
204    if (vs.hasUserData("cs"))
205      CodeSystemUtilities.markStatus((CodeSystem) vs.getUserData("cs"), wg, status, pckage, fmm, normativeVersion);
206    else if (status == StandardsStatus.NORMATIVE && context != null) {
207      for (ConceptSetComponent csc : vs.getCompose().getInclude()) {
208        if (csc.hasSystem()) {
209          CodeSystem cs = context.fetchCodeSystem(csc.getSystem());
210          if (cs != null) {
211            CodeSystemUtilities.markStatus(cs, wg, status, pckage, fmm, normativeVersion);
212          }
213        }
214      }
215    }
216  }
217
218  private static int ssval(String status) {
219    if ("Draft".equals("status")) 
220      return 1;
221    if ("Informative".equals("status")) 
222      return 2;
223    if ("External".equals("status")) 
224      return 3;
225    if ("Trial Use".equals("status")) 
226      return 3;
227    if ("Normative".equals("status")) 
228      return 4;
229    return -1;
230  }
231
232  public static ValueSet generateImplicitValueSet(String uri) {
233    if (uri.startsWith("http://snomed.info/sct"))
234      return generateImplicitSnomedValueSet(uri);
235    if (uri.startsWith("http://loinc.org/vs"))
236      return generateImplicitLoincValueSet(uri);
237    if (uri.equals("http://hl7.org/fhir/ValueSet/mimetypes")) {
238      return generateImplicitMimetypesValueSet(uri);
239    }
240    return null;
241  }
242
243  private static ValueSet generateImplicitMimetypesValueSet(String theUri) {
244    ValueSet valueSet = new ValueSet();
245    valueSet.setStatus(PublicationStatus.ACTIVE);
246    valueSet.setUrl(theUri);
247    valueSet.setDescription("This value set includes all possible codes from BCP-13 (http://tools.ietf.org/html/bcp13)");
248    valueSet.getCompose()
249      .addInclude().setSystem("urn:ietf:bcp:13");
250    return valueSet;
251  }
252
253  private static ValueSet generateImplicitLoincValueSet(String uri) {
254    if ("http://loinc.org/vs".equals(uri))
255      return makeLoincValueSet();
256    if (uri.startsWith("http://loinc.org/vs/LL"))
257      return makeAnswerList(makeLoincValueSet(), uri);
258    return null;
259  }
260
261  private static ValueSet makeAnswerList(ValueSet vs, String uri) {
262    vs.setUrl(uri);
263    String c = uri.substring(20);
264    vs.setName("LOINCAnswers"+c);
265    vs.setTitle("LOINC Answer Codes for "+c);
266    vs.getCompose().getIncludeFirstRep().addFilter().setProperty("LIST").setOp(FilterOperator.EQUAL).setValue(c);
267    return vs;
268  }
269
270  private static ValueSet makeLoincValueSet() {
271    ValueSet vs = new ValueSet();
272    vs.setUrl("http://loinc.org/vs");
273    vs.setName("LOINCCodes");
274    vs.setTitle("All LOINC codes");
275    vs.setCopyright("This content LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at http://loinc.org/terms-of-use");
276    vs.setStatus(PublicationStatus.ACTIVE);
277    vs.getCompose().addInclude().setSystem("http://loinc.org");
278    return vs;
279  }
280
281  private static ValueSet generateImplicitSnomedValueSet(String uri) {
282    if ("http://snomed.info/sct?fhir_vs".equals(uri))
283      return makeImplicitSnomedValueSet(uri);
284    return null;
285  }
286
287  private static ValueSet makeImplicitSnomedValueSet(String uri) {
288    ValueSet vs = new ValueSet();
289    vs.setUrl(uri);
290    vs.setName("SCTValueSet");
291    vs.setTitle("SCT ValueSet");
292    vs.setDescription("All SNOMED CT Concepts");
293    vs.setCopyright("This value set includes content from SNOMED CT, which is copyright © 2002+ International Health Terminology Standards Development Organisation (SNOMED International), and distributed by agreement between SNOMED International and HL7. Implementer use of SNOMED CT is not covered by this agreement");
294    vs.setStatus(PublicationStatus.ACTIVE);
295    vs.getCompose().addInclude().setSystem("http://snomed.info/sct");
296    return vs;
297  }
298
299  public static void setDeprecated(List<ValueSetExpansionPropertyComponent> vsProp,  ValueSetExpansionContainsComponent n) {
300    n.addProperty().setCode("status").setValue(new CodeType("deprecated"));
301    for (ValueSetExpansionPropertyComponent o : vsProp) {
302      if ("status".equals(o.getCode())) {
303        return;
304      }
305    }
306    vsProp.add(new ValueSetExpansionPropertyComponent().setCode("status").setUri("http://hl7.org/fhir/concept-properties#status"));
307  }
308
309
310  public static class ConceptReferenceComponentSorter implements Comparator<ConceptReferenceComponent> {
311
312    @Override
313    public int compare(ConceptReferenceComponent o1, ConceptReferenceComponent o2) {
314      return o1.getCode().compareToIgnoreCase(o2.getCode());
315    }
316  }
317
318
319  public static void sortInclude(ConceptSetComponent inc) {
320    Collections.sort(inc.getConcept(), new ConceptReferenceComponentSorter());
321  }
322
323  public static String getAllCodesSystem(ValueSet vs) {
324    if (vs.hasCompose()) {
325      ValueSetComposeComponent c = vs.getCompose();
326      if (c.getExclude().isEmpty() && c.getInclude().size() == 1) {
327        ConceptSetComponent i = c.getIncludeFirstRep();
328        if (i.hasSystem() && !i.hasValueSet() && !i.hasConcept() && !i.hasFilter()) {
329          return i.getSystem();
330        }
331      }
332    }
333    return null;
334  }
335
336  public static boolean isDeprecated(ValueSet vs, ValueSetExpansionContainsComponent c) {
337    try {
338      for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent p : c.getProperty()) {
339        if ("status".equals(p.getCode()) && p.hasValue() && p.hasValueCodeType() && "deprecated".equals(p.getValueCodeType().getCode())) {
340          return true;
341        }      
342        // this, though status should also be set
343        if ("deprecationDate".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof DateTimeType) 
344          return ((DateTimeType) p.getValue()).before(new DateTimeType(Calendar.getInstance()));
345        // legacy  
346        if ("deprecated".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof BooleanType) 
347          return ((BooleanType) p.getValue()).getValue();
348      }
349      StandardsStatus ss = ToolingExtensions.getStandardsStatus(c);
350      if (ss == StandardsStatus.DEPRECATED) {
351        return true;
352      }
353      return false;
354    } catch (FHIRException e) {
355      return false;
356    }  
357  }
358
359  public static boolean hasCodeInExpansion(ValueSet vs, Coding code) {
360    return hasCodeInExpansion(vs.getExpansion().getContains(), code);
361  }
362
363  private static boolean hasCodeInExpansion(List<ValueSetExpansionContainsComponent> list, Coding code) {
364    for (ValueSetExpansionContainsComponent c : list) {
365      if (c.getSystem().equals(code.getSystem()) && c.getCode().equals(code.getCode())) {
366        return true;
367      }
368      if (hasCodeInExpansion(c.getContains(), code)) {
369        return true;
370      }
371    }
372    return false;
373  }
374
375  public static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent addProperty(ValueSet vs, ValueSetExpansionContainsComponent ctxt, String url, String code, String value) {
376    if (value != null) {
377      return addProperty(vs, ctxt, url, code, new StringType(value));
378    } else {
379      return null;
380    }
381  }
382
383  public static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent addProperty(ValueSet vs, ValueSetExpansionContainsComponent ctxt, String url, String code, Integer value) {
384    if (value != null) {
385      return addProperty(vs, ctxt, url, code, new IntegerType(value));
386    } else {
387      return null;
388    }
389  }
390
391  public static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent addProperty(ValueSet vs, ValueSetExpansionContainsComponent ctxt, String url, String code, DataType value) {
392    code = defineProperty(vs, url, code);
393    org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent p = getProperty(ctxt.getProperty(),  code);
394    if (p != null) {
395      p.setValue(value);
396    } else {
397      p = ctxt.addProperty().setCode(code).setValue(value);
398    }
399    return p;
400  }
401
402  private static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent getProperty(List<org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent> list, String code) {
403    for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent t : list) {
404      if (code.equals(t.getCode())) {
405        return t;
406      }
407    }
408    return null;
409  }
410
411  private static String defineProperty(ValueSet vs, String url, String code) {
412    for (ValueSetExpansionPropertyComponent p : vs.getExpansion().getProperty()) {
413      if (p.hasUri() && p.getUri().equals(url)) {
414        return p.getCode();
415      }
416    }
417    for (ValueSetExpansionPropertyComponent p : vs.getExpansion().getProperty()) {
418      if (p.hasCode() && p.getCode().equals(code)) {
419        p.setUri(url);
420        return code;
421      }
422    }
423    ValueSetExpansionPropertyComponent p = vs.getExpansion().addProperty();
424    p.setUri(url);
425    p.setCode(code);
426    return code;  
427  }
428
429  public static int countExpansion(ValueSet valueset) {
430    int i = valueset.getExpansion().getContains().size();
431    for (ValueSetExpansionContainsComponent t : valueset.getExpansion().getContains()) {
432      i = i + countExpansion(t);
433    }
434    return i;
435  }
436
437  public static int countExpansion(List<ValueSetExpansionContainsComponent> list) {
438    int i = list.size();
439    for (ValueSetExpansionContainsComponent t : list) {
440      i = i + countExpansion(t);
441    }
442    return i;
443  }
444
445  private static int countExpansion(ValueSetExpansionContainsComponent c) {
446    int i = c.getContains().size();
447    for (ValueSetExpansionContainsComponent t : c.getContains()) {
448      i = i + countExpansion(t);
449    }
450    return i;
451  }
452
453  public static Set<String> listSystems(IWorkerContext ctxt, ValueSet vs) {
454    Set<String> systems = new HashSet<>();
455    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
456      for (CanonicalType ct : inc.getValueSet()) {
457        ValueSet vsr = ctxt.findTxResource(ValueSet.class, ct.asStringValue(), vs);
458        if (vsr != null) {
459          systems.addAll(listSystems(ctxt, vsr));
460        }
461      }
462      if (inc.hasSystem()) {
463        systems.add(inc.getSystem());
464      }
465    }
466    return systems;
467  }
468  
469
470  public static boolean isIncompleteExpansion(ValueSet valueSet) {
471    if (valueSet.hasExpansion()) {
472      ValueSetExpansionComponent exp = valueSet.getExpansion();
473      if (exp.hasTotal()) {
474        if (exp.getTotal() != countExpansion(exp.getContains())) {
475          return true;
476        }
477      }
478    }
479    return false;
480  }
481
482
483  public static Set<String> codes(ValueSet vs, CodeSystem cs) {
484    Set<String> res = new HashSet<>();
485    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
486      if (inc.getSystem().equals(cs.getUrl())) {
487        addCodes(res, inc, cs.getConcept());
488      }
489    }
490    return res;
491  }
492
493  private static void addCodes(Set<String> res, ConceptSetComponent inc, List<ConceptDefinitionComponent> list) {
494    for (ConceptDefinitionComponent cd : list) {
495      if (cd.hasCode() && (!inc.hasConcept() || inc.hasConcept(cd.getCode()))) {
496        res.add(cd.getCode());
497      }
498      if (cd.hasConcept()) {
499        addCodes(res, inc, cd.getConcept());
500      }
501    }    
502  }
503}