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