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 lombok.extern.slf4j.Slf4j;
041import org.hl7.fhir.exceptions.FHIRException;
042import org.hl7.fhir.exceptions.FHIRFormatError;
043import org.hl7.fhir.r5.context.IWorkerContext;
044import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
045import org.hl7.fhir.r5.extensions.ExtensionUtilities;
046import org.hl7.fhir.r5.model.BooleanType;
047import org.hl7.fhir.r5.model.CanonicalType;
048import org.hl7.fhir.r5.model.CodeSystem;
049import org.hl7.fhir.r5.model.DateTimeType;
050import org.hl7.fhir.r5.model.StringType;
051import org.hl7.fhir.r5.model.IntegerType;
052import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
053import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
054import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
055import org.hl7.fhir.r5.model.Identifier;
056import org.hl7.fhir.r5.model.Meta;
057import org.hl7.fhir.r5.model.Parameters;
058import org.hl7.fhir.r5.model.UriType;
059import org.hl7.fhir.r5.model.ValueSet;
060import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
061import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
062import org.hl7.fhir.r5.model.CodeType;
063import org.hl7.fhir.r5.model.Coding;
064import org.hl7.fhir.r5.model.DataType;
065import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
066import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
067import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent;
068import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent;
069import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
070import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent;
071import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptDefinitionComponentSorter;
072import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptStatus;
073import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache.SourcedValueSet;
074import org.hl7.fhir.r5.utils.CanonicalResourceUtilities;
075
076import org.hl7.fhir.r5.utils.UserDataNames;
077import org.hl7.fhir.utilities.StandardsStatus;
078import org.hl7.fhir.utilities.Utilities;
079import org.hl7.fhir.utilities.VersionUtilities;
080
081@Slf4j
082public class ValueSetUtilities extends TerminologyUtilities {
083
084
085  public static class ValueSetSorter implements Comparator<ValueSet> {
086
087    @Override
088    public int compare(ValueSet o1, ValueSet o2) {
089      String url1 = o1.getUrl();
090      String url2 = o2.getUrl();
091      int c = compareString(url1, url2);
092      if (c == 0) {
093        String ver1 = o1.getVersion();
094        String ver2 = o2.getVersion();
095        c = VersionUtilities.compareVersions(ver1, ver2);
096        if (c == 0) {
097          String d1 = o1.getDateElement().asStringValue();
098          String d2 = o2.getDateElement().asStringValue();
099          c = compareString(url1, url2);
100        }
101      }
102      return c;
103    }
104
105    private int compareString(String s1, String s2) {
106      if (s1 == null) {
107        return s2 == null ? 0 : 1;
108      } else {
109        return s1.compareTo(s2);
110      }
111    }
112
113  }
114
115
116  public static boolean isServerSide(String url) {
117    return Utilities.existsInList(url, "http://hl7.org/fhir/sid/cvx");
118  }
119  
120  public static ValueSet makeShareable(ValueSet vs) {
121    if (!vs.hasExperimental()) {
122      vs.setExperimental(false);
123    }
124    if (!vs.hasMeta())
125      vs.setMeta(new Meta());
126    for (UriType t : vs.getMeta().getProfile()) 
127      if (t.getValue().equals("http://hl7.org/fhir/StructureDefinition/shareablevalueset"))
128        return vs;
129    vs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablevalueset"));
130    return vs;
131  }
132
133  public static boolean makeVSShareable(ValueSet vs) {
134    if (!vs.hasMeta())
135      vs.setMeta(new Meta());
136    for (UriType t : vs.getMeta().getProfile()) 
137      if (t.getValue().equals("http://hl7.org/fhir/StructureDefinition/shareablevalueset"))
138        return false;
139    vs.getMeta().getProfile().add(new CanonicalType("http://hl7.org/fhir/StructureDefinition/shareablevalueset"));
140    return true;
141  }
142
143  public static void checkShareable(ValueSet vs) {
144    if (!vs.hasMeta())
145      throw new Error("ValueSet "+vs.getUrl()+" is not shareable");
146    for (UriType t : vs.getMeta().getProfile()) {
147      if (t.getValue().equals("http://hl7.org/fhir/StructureDefinition/shareablevalueset"))
148        return;
149    }
150    throw new Error("ValueSet "+vs.getUrl()+" is not shareable");    
151  }
152
153  public static boolean hasOID(ValueSet vs) {
154    return getOID(vs) != null;
155  }
156
157  public static String getOID(ValueSet vs) {
158    for (Identifier id : vs.getIdentifier()) {
159      if ("urn:ietf:rfc:3986".equals(id.getSystem()) && id.hasValue() && id.getValue().startsWith("urn:oid:"))
160        return id.getValue().substring(8);
161    }
162    return null;
163  }
164
165  public static void setOID(ValueSet vs, String oid) {
166    if (!oid.startsWith("urn:oid:"))
167      oid = "urn:oid:" + oid;
168    for (Identifier id : vs.getIdentifier()) {
169      if ("urn:ietf:rfc:3986".equals(id.getSystem()) && id.hasValue() && id.getValue().startsWith("urn:oid:")) {
170        id.setValue(oid);
171        return;
172      }
173    }
174    vs.addIdentifier().setSystem("urn:ietf:rfc:3986").setValue(oid);
175  }
176
177  public static void markStatus(ValueSet vs, String wg, StandardsStatus status, String pckage, String fmm, IWorkerContext context, String normativeVersion) throws FHIRException {
178    if (vs.hasUserData(UserDataNames.render_external_link))
179      return;
180    
181    if (wg != null) {
182      if (!ExtensionUtilities.hasExtension(vs, ExtensionDefinitions.EXT_WORKGROUP) ||
183          (!Utilities.existsInList(ExtensionUtilities.readStringExtension(vs, ExtensionDefinitions.EXT_WORKGROUP), "fhir", "vocab") && Utilities.existsInList(wg, "fhir", "vocab"))) {
184        CanonicalResourceUtilities.setHl7WG(vs, wg);
185      }
186    }
187    if (status != null) {
188      StandardsStatus ss = ExtensionUtilities.getStandardsStatus(vs);
189      if (ss == null || ss.isLowerThan(status)) 
190        ExtensionUtilities.setStandardsStatus(vs, status, normativeVersion);
191      if (pckage != null) {
192        if (!vs.hasUserData(UserDataNames.kindling_ballot_package))        
193          vs.setUserData(UserDataNames.kindling_ballot_package, pckage);
194        else if (!pckage.equals(vs.getUserString(UserDataNames.kindling_ballot_package)))
195          if (!"infrastructure".equals(vs.getUserString(UserDataNames.kindling_ballot_package)))
196          log.warn("Value Set "+vs.getUrl()+": ownership clash "+pckage+" vs "+vs.getUserString(UserDataNames.kindling_ballot_package));
197      }
198      if (status == StandardsStatus.NORMATIVE) {
199        vs.setStatus(PublicationStatus.ACTIVE);
200      }
201    }
202    if (fmm != null) {
203      String sfmm = ExtensionUtilities.readStringExtension(vs, ExtensionDefinitions.EXT_FMM_LEVEL);
204      if (Utilities.noString(sfmm) || Integer.parseInt(sfmm) < Integer.parseInt(fmm))  {
205        ExtensionUtilities.setIntegerExtension(vs, ExtensionDefinitions.EXT_FMM_LEVEL, Integer.parseInt(fmm));
206      }
207    }
208    if (vs.hasUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM))
209      CodeSystemUtilities.markStatus((CodeSystem) vs.getUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM), wg, status, pckage, fmm, normativeVersion);
210    else if (status == StandardsStatus.NORMATIVE && context != null) {
211      for (ConceptSetComponent csc : vs.getCompose().getInclude()) {
212        if (csc.hasSystem()) {
213          CodeSystem cs = context.fetchCodeSystem(csc.getSystem());
214          if (cs != null) {
215            CodeSystemUtilities.markStatus(cs, wg, status, pckage, fmm, normativeVersion);
216          }
217        }
218      }
219    }
220  }
221
222  private static int ssval(String status) {
223    if ("Draft".equals("status")) 
224      return 1;
225    if ("Informative".equals("status")) 
226      return 2;
227    if ("External".equals("status")) 
228      return 3;
229    if ("Trial Use".equals("status")) 
230      return 3;
231    if ("Normative".equals("status")) 
232      return 4;
233    return -1;
234  }
235
236
237  public static class ConceptReferenceComponentSorter implements Comparator<ConceptReferenceComponent> {
238
239    @Override
240    public int compare(ConceptReferenceComponent o1, ConceptReferenceComponent o2) {
241      return o1.getCode().compareToIgnoreCase(o2.getCode());
242    }
243  }
244
245
246  public static void sortInclude(ConceptSetComponent inc) {
247    Collections.sort(inc.getConcept(), new ConceptReferenceComponentSorter());
248  }
249
250  public static String getAllCodesSystem(ValueSet vs) {
251    if (vs.hasCompose()) {
252      ValueSetComposeComponent c = vs.getCompose();
253      if (c.getExclude().isEmpty() && c.getInclude().size() == 1) {
254        ConceptSetComponent i = c.getIncludeFirstRep();
255        if (i.hasSystem() && !i.hasValueSet() && !i.hasConcept() && !i.hasFilter()) {
256          return i.getSystem();
257        }
258      }
259    }
260    return null;
261  }
262
263  public static boolean isDeprecated(ValueSet vs, ValueSetExpansionContainsComponent c) {
264    try {
265      for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent p : c.getProperty()) {
266        if ("status".equals(p.getCode()) && p.hasValue() && p.hasValueCodeType() && "deprecated".equals(p.getValueCodeType().getCode())) {
267          return true;
268        }      
269        // this, though status should also be set
270        if ("deprecationDate".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof DateTimeType) 
271          return ((DateTimeType) p.getValue()).before(new DateTimeType(Calendar.getInstance()));
272        // legacy  
273        if ("deprecated".equals(p.getCode()) && p.hasValue() && p.getValue() instanceof BooleanType) 
274          return ((BooleanType) p.getValue()).getValue();
275      }
276      StandardsStatus ss = ExtensionUtilities.getStandardsStatus(c);
277      if (ss == StandardsStatus.DEPRECATED) {
278        return true;
279      }
280      return false;
281    } catch (FHIRException e) {
282      return false;
283    }  
284  }
285
286  public static boolean hasCodeInExpansion(ValueSet vs, Coding code) {
287    return hasCodeInExpansion(vs.getExpansion().getContains(), code);
288  }
289
290  private static boolean hasCodeInExpansion(List<ValueSetExpansionContainsComponent> list, Coding code) {
291    for (ValueSetExpansionContainsComponent c : list) {
292      if (c.getSystem().equals(code.getSystem()) && c.getCode().equals(code.getCode())) {
293        return true;
294      }
295      if (hasCodeInExpansion(c.getContains(), code)) {
296        return true;
297      }
298    }
299    return false;
300  }
301
302  public static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent addProperty(ValueSet vs, ValueSetExpansionContainsComponent ctxt, String url, String code, String value) {
303    if (value != null) {
304      return addProperty(vs, ctxt, url, code, new StringType(value));
305    } else {
306      return null;
307    }
308  }
309
310  public static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent addProperty(ValueSet vs, ValueSetExpansionContainsComponent ctxt, String url, String code, Integer value) {
311    if (value != null) {
312      return addProperty(vs, ctxt, url, code, new IntegerType(value));
313    } else {
314      return null;
315    }
316  }
317
318  public static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent addProperty(ValueSet vs, ValueSetExpansionContainsComponent ctxt, String url, String code, DataType value) {
319    code = defineProperty(vs, url, code);
320    org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent p = getProperty(ctxt.getProperty(),  code);
321    if (p != null) {
322      p.setValue(value);
323    } else {
324      p = ctxt.addProperty().setCode(code).setValue(value);
325    }
326    return p;
327  }
328
329  private static org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent getProperty(List<org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent> list, String code) {
330    for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent t : list) {
331      if (code.equals(t.getCode())) {
332        return t;
333      }
334    }
335    return null;
336  }
337
338  private static String defineProperty(ValueSet vs, String url, String code) {
339    for (ValueSetExpansionPropertyComponent p : vs.getExpansion().getProperty()) {
340      if (p.hasUri() && p.getUri().equals(url)) {
341        return p.getCode();
342      }
343    }
344    for (ValueSetExpansionPropertyComponent p : vs.getExpansion().getProperty()) {
345      if (p.hasCode() && p.getCode().equals(code)) {
346        p.setUri(url);
347        return code;
348      }
349    }
350    ValueSetExpansionPropertyComponent p = vs.getExpansion().addProperty();
351    p.setUri(url);
352    p.setCode(code);
353    return code;  
354  }
355
356  public static int countExpansion(ValueSet valueset) {
357    int i = valueset.getExpansion().getContains().size();
358    for (ValueSetExpansionContainsComponent t : valueset.getExpansion().getContains()) {
359      i = i + countExpansion(t);
360    }
361    return i;
362  }
363
364  public static int countExpansion(List<ValueSetExpansionContainsComponent> list) {
365    int i = list.size();
366    for (ValueSetExpansionContainsComponent t : list) {
367      i = i + countExpansion(t);
368    }
369    return i;
370  }
371
372  private static int countExpansion(ValueSetExpansionContainsComponent c) {
373    int i = c.getContains().size();
374    for (ValueSetExpansionContainsComponent t : c.getContains()) {
375      i = i + countExpansion(t);
376    }
377    return i;
378  }
379
380  public static Set<String> listSystems(IWorkerContext ctxt, ValueSet vs) {
381    Set<String> systems = new HashSet<>();
382    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
383      for (CanonicalType ct : inc.getValueSet()) {
384        ValueSet vsr = ctxt.findTxResource(ValueSet.class, ct.asStringValue(), vs);
385        if (vsr != null) {
386          systems.addAll(listSystems(ctxt, vsr));
387        }
388      }
389      if (inc.hasSystem()) {
390        systems.add(inc.getSystem());
391      }
392    }
393    return systems;
394  }
395  
396
397  public static boolean isIncompleteExpansion(ValueSet valueSet) {
398    if (valueSet.hasExpansion()) {
399      ValueSetExpansionComponent exp = valueSet.getExpansion();
400      if (exp.hasTotal()) {
401        if (exp.getTotal() != countExpansion(exp.getContains())) {
402          return true;
403        }
404      }
405    }
406    return false;
407  }
408
409
410  public static Set<String> codes(ValueSet vs, CodeSystem cs) {
411    Set<String> res = new HashSet<>();
412    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
413      if (inc.getSystem().equals(cs.getUrl())) {
414        addCodes(res, inc, cs.getConcept());
415      }
416    }
417    return res;
418  }
419
420  private static void addCodes(Set<String> res, ConceptSetComponent inc, List<ConceptDefinitionComponent> list) {
421    for (ConceptDefinitionComponent cd : list) {
422      if (cd.hasCode() && (!inc.hasConcept() || inc.hasConcept(cd.getCode()))) {
423        res.add(cd.getCode());
424      }
425      if (cd.hasConcept()) {
426        addCodes(res, inc, cd.getConcept());
427      }
428    }    
429  }
430  
431  public static String versionFromExpansionParams(Parameters expParameters, String system, String defaultVersion) {
432    if (expParameters != null) {
433      for (ParametersParameterComponent p : expParameters.getParameter()) {
434        if ("system-version".equals(p.getName()) || "force-system-version".equals(p.getName())) {
435          String v = p.getValue().primitiveValue();
436          if (v.startsWith(system+"|")) {
437            String ver = v.substring(v.indexOf("|")+1);
438            if (defaultVersion == null || ver.startsWith(defaultVersion) || "force-system-version".equals(p.getName())) {
439              return ver;
440            }
441          }
442        }
443      }
444    }
445    return defaultVersion;
446  }
447
448  public static Set<String> checkExpansionSubset(ValueSet vs1, ValueSet vs2) {
449    Set<String> codes = new HashSet<>();
450    checkCodes(codes, vs2.getExpansion().getContains(), vs1.getExpansion().getContains());
451    return codes;
452  }
453
454  private static void checkCodes(Set<String> codes, List<ValueSetExpansionContainsComponent> listS, List<ValueSetExpansionContainsComponent> listT) {
455    for (ValueSetExpansionContainsComponent c : listS) {
456      ValueSetExpansionContainsComponent t = findContained(c, listT);
457      if (t == null) {
458        codes.add(c.getCode());
459      }
460      if (c.hasContains()) {
461        checkCodes(codes, c.getContains(), listT);
462      }
463    }
464  }
465
466  private static ValueSetExpansionContainsComponent findContained(ValueSetExpansionContainsComponent c, List<ValueSetExpansionContainsComponent> listT) {
467    for (ValueSetExpansionContainsComponent t : listT) {
468      if (t.getSystem().equals(c.getSystem()) && t.getCode().equals(c.getCode())) {
469        return t;
470      }
471      if (t.hasContains()) {
472        ValueSetExpansionContainsComponent tt = findContained(c, t.getContains());
473        if (tt != null) {
474          return tt;
475        }
476      }
477    }
478    return null;
479  }
480
481  public static void setDeprecated(List<ValueSet.ValueSetExpansionPropertyComponent> vsProp, ValueSet.ValueSetExpansionContainsComponent n) {
482    n.addProperty().setCode("status").setValue(new CodeType("deprecated"));
483    for (ValueSet.ValueSetExpansionPropertyComponent o : vsProp) {
484      if ("status".equals(o.getCode())) {
485        return;
486      }
487    }
488    vsProp.add(new ValueSet.ValueSetExpansionPropertyComponent().setCode("status").setUri("http://hl7.org/fhir/concept-properties#status"));
489  }
490
491
492}