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