001package org.hl7.fhir.r5.terminologies.expansion;
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 static org.apache.commons.lang3.StringUtils.isNotBlank;
035
036import java.io.FileNotFoundException;
037import java.io.IOException;
038import java.text.MessageFormat;
039
040/*
041 * Copyright (c) 2011+, HL7, Inc
042 * All rights reserved.
043 * 
044 * Redistribution and use in source and binary forms, with or without modification,
045 * are permitted provided that the following conditions are met:
046 * 
047 * Redistributions of source code must retain the above copyright notice, this
048 * list of conditions and the following disclaimer.
049 * Redistributions in binary form must reproduce the above copyright notice,
050 * this list of conditions and the following disclaimer in the documentation
051 * and/or other materials provided with the distribution.
052 * Neither the name of HL7 nor the names of its contributors may be used to
053 * endorse or promote products derived from this software without specific
054 * prior written permission.
055 * 
056 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
057 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
058 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
059 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
060 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
061 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
062 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
063 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
064 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
065 * POSSIBILITY OF SUCH DAMAGE.
066 * 
067 */
068
069import java.util.ArrayList;
070import java.util.Calendar;
071import java.util.Collection;
072import java.util.GregorianCalendar;
073import java.util.List;
074
075import org.hl7.fhir.exceptions.FHIRException;
076import org.hl7.fhir.exceptions.FHIRFormatError;
077import org.hl7.fhir.exceptions.NoTerminologyServiceException;
078import org.hl7.fhir.exceptions.TerminologyServiceException;
079import org.hl7.fhir.r5.context.IWorkerContext;
080import org.hl7.fhir.r5.elementmodel.LanguageUtils;
081import org.hl7.fhir.r5.extensions.ExtensionConstants;
082import org.hl7.fhir.r5.extensions.Extensions;
083import org.hl7.fhir.r5.extensions.ExtensionsUtils;
084import org.hl7.fhir.r5.model.BooleanType;
085import org.hl7.fhir.r5.model.CodeSystem;
086import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode;
087import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
088import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
089import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
090import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent;
091import org.hl7.fhir.r5.model.CodeType;
092import org.hl7.fhir.r5.model.Coding;
093import org.hl7.fhir.r5.model.DataType;
094import org.hl7.fhir.r5.model.DateTimeType;
095import org.hl7.fhir.r5.model.DecimalType;
096import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
097import org.hl7.fhir.r5.model.Extension;
098import org.hl7.fhir.r5.model.Factory;
099import org.hl7.fhir.r5.model.IntegerType;
100import org.hl7.fhir.r5.model.PackageInformation;
101import org.hl7.fhir.r5.model.Parameters;
102import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
103import org.hl7.fhir.r5.model.PrimitiveType;
104import org.hl7.fhir.r5.model.Resource;
105import org.hl7.fhir.r5.model.StringType;
106import org.hl7.fhir.r5.model.CanonicalType;
107import org.hl7.fhir.r5.model.UriType;
108import org.hl7.fhir.r5.model.ValueSet;
109import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
110import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
111import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
112import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
113import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent;
114import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent;
115import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
116import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent;
117import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent;
118import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
119import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
120import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpander.Token;
121import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider;
122import org.hl7.fhir.r5.terminologies.providers.CodeSystemProviderExtension;
123import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext;
124import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException;
125import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass;
126import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase;
127import org.hl7.fhir.r5.utils.ToolingExtensions;
128import org.hl7.fhir.r5.utils.client.EFhirClientException;
129import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
130import org.hl7.fhir.utilities.Utilities;
131import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
132import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
133import org.hl7.fhir.utilities.i18n.I18nConstants;
134import org.hl7.fhir.utilities.validation.ValidationOptions;
135
136public class ValueSetExpander extends ValueSetProcessBase {
137
138
139  public class Token {
140    private String system;
141    private String code;
142    public Token(String system, String code) {
143      super();
144      this.system = system;
145      this.code = code;
146    }
147    public String getSystem() {
148      return system;
149    }
150    public String getCode() {
151      return code;
152    }
153    public boolean matches(Coding use) {
154      return system.equals(use.getSystem()) && code.equals(use.getCode());
155    }
156    public boolean matchesLang(String language) {
157      return system.equals("urn:ietf:bcp:47") && code.equals(language);
158    }
159  }
160
161  private static final boolean REPORT_VERSION_ANYWAY = true;
162  
163  private ValueSet focus;
164  private List<String> allErrors = new ArrayList<>();
165  private int maxExpansionSize = 1000;
166  private WorkingContext dwc = new WorkingContext();
167  
168  private boolean checkCodesWhenExpanding;
169  private boolean includeAbstract = true;
170  private boolean debug;
171
172  private AcceptLanguageHeader langs;
173  private List<Token> designations = new ArrayList<>();
174
175  public ValueSetExpander(IWorkerContext context, TerminologyOperationContext opContext) {
176    super(context, opContext);
177  }
178
179  public ValueSetExpander(IWorkerContext context, TerminologyOperationContext opContext, List<String> allErrors) {
180    super(context, opContext);
181    this.allErrors = allErrors;
182  }
183
184  public void setMaxExpansionSize(int theMaxExpansionSize) {
185    maxExpansionSize = theMaxExpansionSize;
186  }
187  
188  private ValueSetExpansionContainsComponent addCode(WorkingContext wc, String system, String code, String display, String dispLang, ValueSetExpansionContainsComponent parent, List<ConceptDefinitionDesignationComponent> designations, Parameters expParams, 
189      boolean isAbstract, boolean inactive, List<ValueSet> filters, boolean noInactive, boolean deprecated, List<ValueSetExpansionPropertyComponent> vsProp, 
190      List<ConceptPropertyComponent> csProps, CodeSystem cs, List<org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent> expProps, List<Extension> csExtList, List<Extension> vsExtList, ValueSetExpansionComponent exp, boolean countToTotal) throws ETooCostly {
191    opContext.deadCheck();
192    
193    if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code, exp))
194      return null;
195    if (noInactive && inactive) {
196      return null;
197    }
198    
199    ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
200    n.setSystem(system);
201    n.setCode(code);
202    if (isAbstract) {
203      n.setAbstract(true);
204    }
205    if (inactive) {
206      n.setInactive(true);
207    }
208    if (deprecated) {
209      ValueSetUtilities.setDeprecated(vsProp, n);
210    }
211    if (ExtensionsUtils.hasExtension(csExtList, "http://hl7.org/fhir/StructureDefinition/codesystem-label")) {
212      ValueSetUtilities.addProperty(focus, n, "http://hl7.org/fhir/concept-properties#label", "label", ExtensionsUtils.getExtensionValue(csExtList, "http://hl7.org/fhir/StructureDefinition/codesystem-label"));
213    }
214    if (ExtensionsUtils.hasExtension(vsExtList, "http://hl7.org/fhir/StructureDefinition/valueset-label")) {
215      ValueSetUtilities.addProperty(focus, n, "http://hl7.org/fhir/concept-properties#label", "label", ExtensionsUtils.getExtensionValue(vsExtList, "http://hl7.org/fhir/StructureDefinition/valueset-label"));
216    }
217    if (ExtensionsUtils.hasExtension(csExtList, "http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder")) {
218      ValueSetUtilities.addProperty(focus, n, "http://hl7.org/fhir/concept-properties#order", "order", convertToDecimal(ExtensionsUtils.getExtensionValue(csExtList, "http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder")));
219    }
220    if (ExtensionsUtils.hasExtension(vsExtList, "http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder")) {
221      ValueSetUtilities.addProperty(focus, n, "http://hl7.org/fhir/concept-properties#order", "order", convertToDecimal(ExtensionsUtils.getExtensionValue(vsExtList, "http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder")));
222    }
223    if (ExtensionsUtils.hasExtension(csExtList, "http://hl7.org/fhir/StructureDefinition/itemWeight")) {
224      ValueSetUtilities.addProperty(focus, n, "http://hl7.org/fhir/concept-properties#itemWeight", "weight", ExtensionsUtils.getExtensionValue(csExtList, "http://hl7.org/fhir/StructureDefinition/itemWeight"));
225    }
226    if (ExtensionsUtils.hasExtension(vsExtList, "http://hl7.org/fhir/StructureDefinition/itemWeight")) {
227      ValueSetUtilities.addProperty(focus, n, "http://hl7.org/fhir/concept-properties#itemWeight", "weight", ExtensionsUtils.getExtensionValue(vsExtList, "http://hl7.org/fhir/StructureDefinition/itemWeight"));
228    }
229    ExtensionsUtils.copyExtensions(csExtList, n.getExtension(), 
230        "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", 
231        "http://hl7.org/fhir/StructureDefinition/rendering-style", 
232        "http://hl7.org/fhir/StructureDefinition/rendering-xhtml",
233        "http://hl7.org/fhir/StructureDefinition/codesystem-alternate");
234    
235    ExtensionsUtils.copyExtensions(vsExtList, n.getExtension(), 
236        "http://hl7.org/fhir/StructureDefinition/valueset-supplement", 
237        "http://hl7.org/fhir/StructureDefinition/valueset-deprecated",
238        "http://hl7.org/fhir/StructureDefinition/valueset-concept-definition",
239        "http://hl7.org/fhir/StructureDefinition/coding-sctdescid", 
240        "http://hl7.org/fhir/StructureDefinition/rendering-style", 
241        "http://hl7.org/fhir/StructureDefinition/rendering-xhtml");
242    
243    // display and designations
244    ConceptDefinitionDesignationComponent pref = null;
245    if (langs == null) {
246      n.setDisplay(display);
247    } else {
248      if (designations == null) {
249        designations = new ArrayList<>();
250      }
251      designations.add(new ConceptDefinitionDesignationComponent().setLanguage(dispLang).setValue(display).setUse(new Coding().setSystem("http://terminology.hl7.org/CodeSystem/designation-usage").setCode("display")));
252      pref = findMatchingDesignation(designations);
253      if (pref != null) {
254        n.setDisplay(pref.getValue());
255      }
256    }
257
258    if (expParams.getParameterBool("includeDesignations") && designations != null) {
259      
260      for (ConceptDefinitionDesignationComponent t : designations) {
261        if (t != pref && (t.hasLanguage() || t.hasUse()) && t.getValue() != null && passesDesignationFilter(t)) {
262          ConceptReferenceDesignationComponent d = n.addDesignation();
263          if (t.getLanguage() != null) {
264            d.setLanguage(t.getLanguage().trim());
265          }
266          if (t.getValue() != null) {
267            d.setValue(t.getValue().trim());
268          }
269          if (t.getUse() != null) {
270            d.setUse(t.getUse());
271          }
272          for (Extension ext : t.getExtension()) {
273            if (Utilities.existsInList(ext.getUrl(), "http://hl7.org/fhir/StructureDefinition/coding-sctdescid")) {
274              d.addExtension(ext);
275            }
276          }
277        }
278      }
279    }
280    for (ParametersParameterComponent p : expParams.getParameter()) {
281      if ("property".equals(p.getName())) {
282        if (csProps != null && p.hasValue()) {
283          for (ConceptPropertyComponent cp : csProps) {
284            if (p.getValue().primitiveValue().equals(cp.getCode())) {
285              PropertyComponent pd = cs.getProperty(cp.getCode());
286              String url = pd == null ? null : pd.getUri();
287              if (url == null) {
288                if ("definition".equals(cp.getCode())) {
289                  url = "http://hl7.org/fhir/concept-properties#definition";
290                } else {
291                  // ??
292                }
293              }
294              ValueSetUtilities.addProperty(focus, n, url, cp.getCode(), cp.getValue()).copyExtensions(cp, "http://hl7.org/fhir/StructureDefinition/alternate-code-use", "http://hl7.org/fhir/StructureDefinition/alternate-code-status");
295            }
296          }
297        }
298        if (expProps != null && p.hasValue()) {
299          for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent cp : expProps) {
300            if (p.getValue().primitiveValue().equals(cp.getCode())) {
301              String url = null;
302              for (ValueSetExpansionPropertyComponent t : vsProp) {
303                if (t.hasCode() && t.getCode().equals(cp.getCode())) {
304                  url = t.getUri();
305                }
306              }
307              if (url == null) {
308                if ("definition".equals(cp.getCode())) {
309                  url = "http://hl7.org/fhir/concept-properties#definition";
310                } else {
311                  // TODO: try looking it up from the code system
312                }
313              }
314              ValueSetUtilities.addProperty(focus, n, url, cp.getCode(), cp.getValue()).copyExtensions(cp, "http://hl7.org/fhir/StructureDefinition/alternate-code-use", "http://hl7.org/fhir/StructureDefinition/alternate-code-status");
315            }
316          }
317        }        
318      }
319    }
320
321    String s = key(n);
322    if (wc.getMap().containsKey(s) || wc.getExcludeKeys().contains(s)) {
323      wc.setCanBeHierarchy(false);
324    } else {
325      wc.getCodes().add(n);
326      wc.getMap().put(s, n);
327      if (countToTotal) {
328        wc.incTotal();
329      }
330//      if (wc == dwc && wc.getTotal() > maxExpansionSize) {
331//        if (wc.getOffset()+wc.getCount() > 0 && wc.getTotal() > wc.getOffset()+wc.getCount()) {
332//          wc.setTotal(-1);
333//          throw new EFinished();
334//        }
335//        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, focus.getUrl(), ">" + Integer.toString(maxExpansionSize)));
336//      }
337    }
338    if (wc.isCanBeHierarchy() && parent != null) {
339      parent.getContains().add(n);
340    } else if (!wc.getRootMap().containsKey(s)) {
341      wc.getRootMap().put(s, n);
342      wc.getRoots().add(n);
343    }
344    return n;
345  }
346
347  private DataType convertToDecimal(DataType v) {
348    if (v == null) {
349      return null;
350    } 
351    if (v instanceof DecimalType) {
352      return v;
353    } 
354    if (v instanceof IntegerType) {
355      return new DecimalType(((IntegerType) v).asStringValue());
356    }
357    return null;
358  }
359
360  private boolean passesDesignationFilter(ConceptDefinitionDesignationComponent d) {
361    if (designations.isEmpty()) {
362      return true;
363    }
364    for (Token t : designations) {
365      if (t.matches(d.getUse()) || t.matchesLang(d.getLanguage())) {
366        return true;
367      }
368      for (Coding c : d.getAdditionalUse()) {
369        if (t.matches(c)) {
370          return true;
371        }
372      }
373    }
374    return false;
375  }
376
377  private ConceptDefinitionDesignationComponent findMatchingDesignation(List<ConceptDefinitionDesignationComponent> designations) {
378    if (langs == null) {
379      return null;
380    }
381    // we have a list of languages in priority order 
382    // we have a list of designations in no order 
383    // language exact match is preferred
384    // display is always preferred
385    
386    for (LanguagePreference lang : langs.getLangs()) {
387      if (lang.getValue() > 0) {
388        for (ConceptDefinitionDesignationComponent cd : designations) {
389          if (isDisplay(cd) && LanguageUtils.langsMatchExact(cd.getLanguage(), lang.getLang())) {
390            return cd;
391          }
392        }
393        for (ConceptDefinitionDesignationComponent cd : designations) {
394          if (isDisplay(cd) && LanguageUtils.langsMatch(cd.getLanguage(), lang.getLang())) {
395            return cd;
396          }
397        }
398        for (ConceptDefinitionDesignationComponent cd : designations) {
399          if (LanguageUtils.langsMatchExact(cd.getLanguage(), lang.getLang())) {
400            return cd;
401          }
402        }
403        for (ConceptDefinitionDesignationComponent cd : designations) {
404          if (LanguageUtils.langsMatch(cd.getLanguage(), lang.getLang())) {
405            return cd;
406          }
407        }
408      }
409    }
410    return null;
411  }
412
413  private boolean isDisplay(ConceptDefinitionDesignationComponent cd) {
414    return cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "display");
415  }
416
417  private boolean filterContainsCode(List<ValueSet> filters, String system, String code, ValueSetExpansionComponent exp) {
418    for (ValueSet vse : filters) {
419      checkCanonical(exp, vse, focus);
420      if (expansionContainsCode(vse.getExpansion().getContains(), system, code))
421        return true;
422    }
423    return false;
424  }
425
426  private boolean expansionContainsCode(List<ValueSetExpansionContainsComponent> contains, String system, String code) {
427    for (ValueSetExpansionContainsComponent cc : contains) {
428      if (system.equals(cc.getSystem()) && code.equals(cc.getCode()))
429        return true;
430      if (expansionContainsCode(cc.getContains(), system, code))
431        return true;
432    }
433    return false;
434  }
435
436  private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, AcceptLanguageHeader langs) {
437    for (ConceptDefinitionDesignationComponent t : list) {
438      if (LanguageUtils.langsMatchExact(langs, t.getLanguage())) {
439        return t;
440      }
441    }
442    for (ConceptDefinitionDesignationComponent t : list) {
443      if (LanguageUtils.langsMatch(langs, t.getLanguage())) {
444        return t;
445      }
446    }
447    return null;
448  }
449
450  private void addCodeAndDescendents(WorkingContext wc, ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp, boolean countToTotal)  throws FHIRException, ETooCostly {
451    opContext.deadCheck();
452    focus.checkNoModifiers("Expansion.contains", "expanding");
453    ValueSetExpansionContainsComponent np = null;
454    for (String code : getCodesForConcept(focus, expParams)) {
455      ValueSetExpansionContainsComponent t = addCode(wc, focus.getSystem(), code, focus.getDisplay(), vsSrc.getLanguage(), parent, 
456           convert(focus.getDesignation()), expParams, focus.getAbstract(), focus.getInactive(), filters, noInactive, false, vsProps, makeCSProps(focus.getExtensionString(ToolingExtensions.EXT_DEFINITION), null), null, focus.getProperty(), null, focus.getExtension(), exp, countToTotal);
457      if (np == null) {
458        np = t;
459      }
460    }
461    for (ValueSetExpansionContainsComponent c : focus.getContains())
462      addCodeAndDescendents(wc, c, np, expParams, filters, noInactive, vsProps, vsSrc, exp, countToTotal);
463  }
464  
465  private List<ConceptPropertyComponent> makeCSProps(String definition, List<ConceptPropertyComponent> list) {
466    List<ConceptPropertyComponent> res = new ArrayList<>();
467    if (!Utilities.noString(definition)) {
468      res.add(new ConceptPropertyComponent("definition", new StringType(definition)));
469    }
470    if (list != null) {
471      res.addAll(list);
472    }
473    return res;
474  }
475
476  private List<String> getCodesForConcept(ValueSetExpansionContainsComponent focus, Parameters expParams) {
477    List<String> codes = new ArrayList<>();
478    codes.add(focus.getCode());
479    for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent p : focus.getProperty()) {
480      if ("alternateCode".equals(p.getCode()) && (altCodeParams.passes(p.getExtension())) && p.getValue().isPrimitive()) {
481        codes.add(p.getValue().primitiveValue());        
482      }
483    }
484    return codes;
485  }
486
487  private List<ConceptDefinitionDesignationComponent> convert(List<ConceptReferenceDesignationComponent> designations) {
488    List<ConceptDefinitionDesignationComponent> list = new ArrayList<ConceptDefinitionDesignationComponent>();
489    for (ConceptReferenceDesignationComponent d : designations) {
490      ConceptDefinitionDesignationComponent n = new ConceptDefinitionDesignationComponent();
491      n.setLanguage(d.getLanguage());
492      n.setUse(d.getUse());
493      n.setValue(d.getValue());
494      list.add(n);
495    }
496    return list;
497  }
498
499  private void addCodeAndDescendents(WorkingContext wc, CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, 
500        ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps, List<WorkingContext> otherFilters, ValueSetExpansionComponent exp)  throws FHIRException, ETooCostly {
501    opContext.deadCheck();
502    def.checkNoModifiers("Code in Code System", "expanding");
503    if (exclusion != null) {
504      if (exclusion.getCode().equals(def.getCode()))
505        return; // excluded.
506    }
507    ValueSetExpansionContainsComponent np = null;
508    boolean abs = CodeSystemUtilities.isNotSelectable(cs, def);
509    boolean inc = CodeSystemUtilities.isInactive(cs, def);
510    boolean dep = CodeSystemUtilities.isDeprecated(cs, def, false);
511    if ((includeAbstract || !abs)  && filterFunc.includeConcept(cs, def) && passesOtherFilters(otherFilters, cs, def.getCode())) {
512      for (String code : getCodesForConcept(def, expParams)) {
513        ValueSetExpansionContainsComponent t = addCode(wc, system, code, def.getDisplay(), cs.getLanguage(), parent, def.getDesignation(), expParams, abs, inc, filters, noInactive, dep, vsProps, makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), null, exp, true);
514        if (np == null) {
515          np = t;
516        }
517      }
518    }
519    for (ConceptDefinitionComponent c : def.getConcept()) {
520      addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters, exp);
521    }
522    if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
523      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
524      for (ConceptDefinitionComponent c : children)
525        addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters, exp);
526    }
527  }
528
529
530  private void excludeCodeAndDescendents(WorkingContext wc, CodeSystem cs, String system, ConceptDefinitionComponent def, Parameters expParams, List<ValueSet> filters, 
531      ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, List<WorkingContext> otherFilters, ValueSetExpansionComponent exp)  throws FHIRException, ETooCostly {
532    opContext.deadCheck();
533    def.checkNoModifiers("Code in Code System", "expanding");
534    if (exclusion != null) {
535      if (exclusion.getCode().equals(def.getCode()))
536        return; // excluded.
537    }
538    boolean abs = CodeSystemUtilities.isNotSelectable(cs, def);
539    if ((includeAbstract || !abs)  && filterFunc.includeConcept(cs, def) && passesOtherFilters(otherFilters, cs, def.getCode())) {
540      for (String code : getCodesForConcept(def, expParams)) {
541        if (!(filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code, exp)))
542          excludeCode(wc, system, code);
543      }
544    }
545    for (ConceptDefinitionComponent c : def.getConcept()) {
546      excludeCodeAndDescendents(wc, cs, system, c, expParams, filters, exclusion, filterFunc, otherFilters, exp);
547    }
548    if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
549      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
550      for (ConceptDefinitionComponent c : children)
551        excludeCodeAndDescendents(wc, cs, system, c, expParams, filters, exclusion, filterFunc, otherFilters, exp);
552    }
553  }
554
555
556  private List<String> getCodesForConcept(ConceptDefinitionComponent focus, Parameters expParams) {
557    List<String> codes = new ArrayList<>();
558    codes.add(focus.getCode());
559    for (ConceptPropertyComponent p : focus.getProperty()) {
560      if ("alternateCode".equals(p.getCode()) && (altCodeParams.passes(p.getExtension())) && p.getValue().isPrimitive()) {
561        codes.add(p.getValue().primitiveValue());        
562      }
563    }
564    return codes;
565  }
566
567  private static boolean hasUse(ConceptPropertyComponent p, List<String> uses) {
568    for (Extension ext : p.getExtensionsByUrl(ToolingExtensions.EXT_CS_ALTERNATE_USE)) {
569      if (ext.hasValueCoding() && Utilities.existsInList(ext.getValueCoding().getCode(), uses)) {
570        return true;
571      }
572    }
573    return false;
574  }
575
576  
577
578  private void addCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws ETooCostly, FHIRException {
579    if (expand != null) {
580      if (expand.getContains().size() > maxExpansionSize)
581        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, vsSrc.getUrl(), ">" + Integer.toString(expand.getContains().size())));
582      for (ValueSetExpansionParameterComponent p : expand.getParameter()) {
583        if (!existsInParams(params, p.getName(), p.getValue()))
584          params.add(p);
585      }
586      
587      copyImportContains(expand.getContains(), null, expParams, filters, noInactive, vsProps, vsSrc, exp);
588    }
589  }
590
591  private void excludeCode(WorkingContext wc, String theSystem, String theCode) {
592    ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
593    n.setSystem(theSystem);
594    n.setCode(theCode);
595    String s = key(n);
596    wc.getExcludeKeys().add(s);
597  }
598
599  private void excludeCodes(WorkingContext wc, ConceptSetComponent exc, Parameters expParams, ValueSetExpansionComponent exp, ValueSet vs) throws FHIRException, FileNotFoundException, ETooCostly, IOException {
600    opContext.deadCheck();
601    exc.checkNoModifiers("Compose.exclude", "expanding");
602    if (exc.hasSystem() && exc.getConcept().size() == 0 && exc.getFilter().size() == 0) {
603      wc.getExcludeSystems().add(exc.getSystem());
604    }
605
606    for (UriType imp : exc.getValueSet()) {
607      excludeCodes(wc, importValueSetForExclude(wc, imp.getValue(), exp, expParams, false, vs).getExpansion());
608    }
609    
610    CodeSystem cs = context.fetchSupplementedCodeSystem(exc.getSystem());
611    if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(exc.getSystem(), opContext.getOptions().getFhirVersion())) {
612      ValueSetExpansionOutcome vse = context.expandVS(exc, false, false);
613      ValueSet valueset = vse.getValueset();
614      if (valueset == null)
615        throw failTSE("Error Expanding ValueSet: "+vse.getError());
616      excludeCodes(wc, valueset.getExpansion());
617      return;
618    }
619
620    for (ConceptReferenceComponent c : exc.getConcept()) {
621      excludeCode(wc, exc.getSystem(), c.getCode());
622    }
623
624    if (exc.getFilter().size() > 0) {
625      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
626        addFragmentWarning(exp, cs);
627      }
628      List<WorkingContext> filters = new ArrayList<>();
629      for (int i = 1; i < exc.getFilter().size(); i++) {
630        WorkingContext wc1 = new WorkingContext();
631        filters.add(wc1);
632        processFilter(exc, exp, expParams, null, cs, false, exc.getFilter().get(i), wc1, null, true);
633      }
634      ConceptSetFilterComponent fc = exc.getFilter().get(0);
635      WorkingContext wc1 = dwc;
636      processFilter(exc, exp, expParams, null, cs, false, fc, wc1, filters, true);
637    }
638  }
639
640  private void excludeCodes(WorkingContext wc, ValueSetExpansionComponent expand) {
641    opContext.deadCheck();
642    for (ValueSetExpansionContainsComponent c : expand.getContains()) {
643      excludeCode(wc, c.getSystem(), c.getCode());
644    }
645  }
646
647  private boolean existsInParams(List<ValueSetExpansionParameterComponent> params, String name, DataType value) {
648    for (ValueSetExpansionParameterComponent p : params) {
649      if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) {
650        return true;
651      }
652    }
653    return false;
654  }
655
656  public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) {
657    
658    allErrors.clear();
659    try {
660      opContext.seeContext(source.getVersionedUrl());
661      
662      return expandInternal(source, expParams);
663    } catch (NoTerminologyServiceException e) {
664      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
665      // that might fail too, but it might not, later.
666      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.NOSERVICE, allErrors, false);
667    } catch (CodeSystemProviderExtension e) {
668      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
669      // that might fail too, but it might not, later.
670      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.INTERNAL_ERROR, allErrors, false);
671    } catch (TerminologyServiceProtectionException e) {
672      if (opContext.isOriginal()) {
673        return new ValueSetExpansionOutcome(e.getMessage(), e.getError(), allErrors, false);
674      } else {
675        throw e;
676      }
677    } catch (ETooCostly e) {
678      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.TOO_COSTLY, allErrors, false);
679    } catch (Exception e) {
680      if (debug) {
681        e.printStackTrace();
682      }
683      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.UNKNOWN, allErrors, e instanceof EFhirClientException || e instanceof TerminologyServiceException);
684    }
685  }
686  
687  public ValueSetExpansionOutcome expandInternal(ValueSet source, Parameters expParams) throws FHIRException, FileNotFoundException, ETooCostly, IOException, CodeSystemProviderExtension {
688      return doExpand(source, expParams);
689  }
690
691  private void processParameter(String name, DataType value) {
692    if (Utilities.existsInList(name, "includeDesignations", "excludeNested", "activeOnly", "offset", "count")) {
693      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
694      focus.getExpansion().addParameter().setName(name).setValue(value);
695    }
696    if ("displayLanguage".equals(name)) {
697      this.langs = new AcceptLanguageHeader(value.primitiveValue(), true);
698      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
699      focus.getExpansion().addParameter().setName(name).setValue(new CodeType(value.primitiveValue()));
700    }
701    if ("designation".equals(name)) {
702      String[] v = value.primitiveValue().split("\\|");
703      if (v.length != 2 || !Utilities.isAbsoluteUrl(v[0]) || Utilities.noString(v[1])) {
704        throw new NoTerminologyServiceException("Unable to understand designation parameter "+value.primitiveValue());
705      }
706      this.designations.add(new Token(v[0], v[1]));
707      focus.getExpansion().addParameter().setName(name).setValue(new StringType(value.primitiveValue()));
708    }
709    if ("offset".equals(name) && value instanceof IntegerType) {
710      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
711      focus.getExpansion().addParameter().setName(name).setValue(value);
712      dwc.setOffsetParam(((IntegerType) value).getValue());
713      if (dwc.getOffsetParam() < 0) {
714        dwc.setOffsetParam(0);
715      }
716    }
717    if ("count".equals(name)) {
718      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
719      focus.getExpansion().addParameter().setName(name).setValue(value);
720      dwc.setCountParam(((IntegerType) value).getValue());
721      if (dwc.getCountParam() < 0) {
722        dwc.setCountParam(0);
723      }
724    }
725  }
726  
727  public ValueSetExpansionOutcome doExpand(ValueSet source, Parameters expParams) throws FHIRException, ETooCostly, FileNotFoundException, IOException, CodeSystemProviderExtension {
728    if (expParams == null)
729      expParams = makeDefaultExpansion();
730    altCodeParams.seeParameters(expParams);
731    altCodeParams.seeValueSet(source);
732    
733    source.checkNoModifiers("ValueSet", "expanding");
734    focus = source.copy();
735    focus.setIdBase(null);
736    focus.setExpansion(new ValueSet.ValueSetExpansionComponent());
737    focus.getExpansion().setTimestampElement(DateTimeType.now());
738    focus.getExpansion().setIdentifier(Factory.createUUID()); 
739    checkCanonical(focus.getExpansion(), focus, focus);
740    for (Extension ext : focus.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) {
741      processParameter(ext.getExtensionString("name"), ext.getExtensionByUrl("value").getValue());
742    }
743    for (ParametersParameterComponent p : expParams.getParameter()) {
744      processParameter(p.getName(), p.getValue());
745    }
746    for (Extension s : focus.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) {
747      requiredSupplements.add(s.getValue().primitiveValue());
748    }
749    if (langs == null && focus.hasLanguage()) {
750      langs = new AcceptLanguageHeader(focus.getLanguage(), true);
751    }
752
753    try {
754      if (source.hasCompose()) {
755//        ExtensionsUtils.stripExtensions(focus.getCompose()); - disabled 23/05/2023 GDG - why was this ever thought to be a good idea?
756        handleCompose(source.getCompose(), focus.getExpansion(), expParams, source.getUrl(), focus.getExpansion().getExtension(), source);
757      }
758    } catch (EFinished e) {
759      // nothing - we intended to trap this here
760    }
761
762    if (dwc.getTotal() > maxExpansionSize && dwc.getOffsetParam() + dwc.getCountParam() == 0) {
763      if (dwc.isNoTotal()) {
764        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, focus.getVersionedUrl(), ">" + MessageFormat.format("{0,number,#}", maxExpansionSize)));        
765      } else {
766        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY_COUNT, focus.getVersionedUrl(), ">" + MessageFormat.format("{0,number,#}", maxExpansionSize), MessageFormat.format("{0,number,#}", dwc.getTotal())));
767      }
768    } else if (dwc.isCanBeHierarchy() && ((dwc.getCountParam() == 0) || dwc.getCountParam() > dwc.getCodes().size())) {
769      for (ValueSetExpansionContainsComponent c : dwc.getRoots()) {
770        focus.getExpansion().getContains().add(c);
771      }
772    } else {
773      int i = 0;
774      int cc = 0;
775      for (ValueSetExpansionContainsComponent c : dwc.getCodes()) {
776        c.getContains().clear(); // make sure any hierarchy is wiped
777        if (dwc.getMap().containsKey(key(c)) && (includeAbstract || !c.getAbstract())) { // we may have added abstract codes earlier while we still thought it might be heirarchical, but later we gave up, so now ignore them
778          if (dwc.getOffsetParam() == 0 || i >= dwc.getOffsetParam()) {
779            focus.getExpansion().getContains().add(c);
780            cc++;
781            if (cc == dwc.getCountParam()) {
782              break;
783            }
784          }
785          i++;
786        }
787      }
788    }
789
790    if (dwc.hasOffsetParam()) {
791      focus.getExpansion().setOffset(dwc.getOffsetParam());
792    }
793    if (!dwc.isNoTotal()) {
794      focus.getExpansion().setTotal(dwc.getTotal());
795    }
796    if (!requiredSupplements.isEmpty()) {      
797      return new ValueSetExpansionOutcome(context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements)), TerminologyServiceErrorClass.BUSINESS_RULE, allErrors, false);
798    }
799    if (!expParams.hasParameter("includeDefinition") || !expParams.getParameterBool("includeDefinition")) {
800      focus.setCompose(null);
801      focus.getExtension().clear();
802      focus.setPublisher(null);
803      focus.setDescription(null);
804      focus.setPurpose(null);
805      focus.getContact().clear();
806      focus.setCopyright(null);
807      focus.setText(null);
808    }
809    return new ValueSetExpansionOutcome(focus);
810  }
811
812
813  private Parameters makeDefaultExpansion() {
814    Parameters res = new Parameters();
815    res.addParameter("excludeNested", true);
816    res.addParameter("includeDesignations", false);
817    return res;
818  }
819
820  private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> clist, String code) {
821    for (ConceptDefinitionComponent c : clist) {
822      if (code.equals(c.getCode()))
823        return c;
824      ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code);
825      if (v != null)
826        return v;
827    }
828    return null;
829  }
830
831  private void handleCompose(ValueSetComposeComponent compose, ValueSetExpansionComponent exp, Parameters expParams, String ctxt, List<Extension> extensions, ValueSet valueSet)
832      throws ETooCostly, FileNotFoundException, IOException, FHIRException, CodeSystemProviderExtension {
833    compose.checkNoModifiers("ValueSet.compose", "expanding");
834    // Exclude comes first because we build up a map of things to exclude
835    for (ConceptSetComponent inc : compose.getExclude())
836      excludeCodes(dwc, inc, expParams, exp, valueSet);
837    dwc.setCanBeHierarchy(!expParams.getParameterBool("excludeNested") && dwc.getExcludeKeys().isEmpty() && dwc.getExcludeSystems().isEmpty() && dwc.getOffsetParam() == 0);
838    includeAbstract = !expParams.getParameterBool("excludeNotForUI");
839    boolean first = true;
840    for (ConceptSetComponent inc : compose.getInclude()) {
841      if (first == true)
842        first = false;
843      else
844        dwc.setCanBeHierarchy(false);
845      includeCodes(inc, exp, expParams, dwc.isCanBeHierarchy(), compose.hasInactive() ? !compose.getInactive() : checkNoInActiveFromParam(expParams), extensions, valueSet);
846    }
847  }
848
849  /**
850   * returns true if activeOnly = true 
851   * @param expParams
852   * @return
853   */
854  private boolean checkNoInActiveFromParam(Parameters expParams) {
855    for (ParametersParameterComponent p : expParams.getParameter()) {
856      if (p.getName().equals("activeOnly")) {
857        return p.getValueBooleanType().getValue();
858      }
859    }
860    return false;
861  }
862
863  private ValueSet importValueSet(WorkingContext wc, String value, ValueSetExpansionComponent exp, Parameters expParams, boolean noInactive, ValueSet valueSet) throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError {
864    if (value == null)
865      throw fail("unable to find value set with no identity");
866    ValueSet vs = context.findTxResource(ValueSet.class, value, valueSet);
867    if (vs == null) {
868      if (context.fetchResource(CodeSystem.class, value, valueSet) != null) {
869        throw fail("Cannot include value set "+value+" because it's actually a code system");
870      } else {
871        throw fail("Unable to find imported value set " + value);
872      }
873    }
874    checkCanonical(exp, vs, focus);
875    if (noInactive) {
876      expParams = expParams.copy();
877      expParams.addParameter("activeOnly", true);
878    }
879    ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams);
880    if (vso.getError() != null) {
881      addErrors(vso.getAllErrors());
882      throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError());
883    } else if (vso.getValueset() == null) {
884      throw fail("Unable to expand imported value set "+vs.getUrl()+" but no error");      
885    }
886    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
887      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
888      if (!existsInParams(exp.getParameter(), "used-valueset", u))
889        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
890    }
891    ValueSetExpansionComponent evs = vso.getValueset().getExpansion();
892    for (Extension ex : evs.getExtension()) {
893      if (ex.getUrl().equals(ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
894        if (ex.getValue() instanceof BooleanType) {
895          exp.getExtension().add(new Extension(ToolingExtensions.EXT_EXP_TOOCOSTLY).setValue(new CanonicalType(value)));
896        } else {
897          exp.getExtension().add(ex);
898        }
899      } 
900    }
901    if (evs.hasTotal()) {
902      dwc.incTotal(evs.getTotal());
903    } else {
904      dwc.setNoTotal(true);
905    }
906    for (ValueSetExpansionParameterComponent p : evs.getParameter()) {
907      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue()))
908        exp.getParameter().add(p);
909    }
910    if (isValueSetUnionImports(valueSet)) {
911      copyExpansion(wc, evs.getContains());
912    }
913    wc.setCanBeHierarchy(false); // if we're importing a value set, we have to be combining, so we won't try for a hierarchy
914    return vso.getValueset();
915  }
916  
917
918  private ValueSet importValueSetForExclude(WorkingContext wc, String value, ValueSetExpansionComponent exp, Parameters expParams, boolean noInactive, ValueSet valueSet) throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError {
919    if (value == null)
920      throw fail("unable to find value set with no identity");
921    ValueSet vs = context.findTxResource(ValueSet.class, value, valueSet);
922    if (vs == null) {
923      if (context.fetchResource(CodeSystem.class, value, valueSet) != null) {
924        throw fail("Cannot include value set "+value+" because it's actually a code system");
925      } else {
926        throw fail("Unable to find imported value set " + value);
927      }
928    }
929    checkCanonical(exp, vs, focus);
930    if (noInactive) {
931      expParams = expParams.copy();
932      expParams.addParameter("activeOnly", true);
933    }
934    ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams);
935    if (vso.getError() != null) {
936      addErrors(vso.getAllErrors());
937      throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError());
938    }
939    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
940      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
941      if (!existsInParams(exp.getParameter(), "used-valueset", u))
942        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
943    }
944    for (Extension ex : vso.getValueset().getExpansion().getExtension()) {
945      if (ex.getUrl().equals(ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
946        throw fail("Unable to expand imported value set "+vs.getUrl()+" for exclude: too costly");
947      } 
948    }
949    return vso.getValueset();
950  }
951  
952  protected boolean isValueSetUnionImports(ValueSet valueSet) {
953    PackageInformation p = valueSet.getSourcePackage();
954    if (p != null) {
955      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
956    } else {
957      return false;
958    }
959  }
960
961  public void copyExpansion(WorkingContext wc,List<ValueSetExpansionContainsComponent> list) {
962    opContext.deadCheck();
963    for (ValueSetExpansionContainsComponent cc : list) {
964       ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
965       n.setSystem(cc.getSystem());
966       n.setCode(cc.getCode());
967       n.setAbstract(cc.getAbstract());
968       n.setInactive(cc.getInactive());
969       n.setDisplay(cc.getDisplay());
970       n.getDesignation().addAll(cc.getDesignation());
971
972       String s = key(n);
973       if (!wc.getMap().containsKey(s) && !wc.getExcludeKeys().contains(s)) {
974         wc.getCodes().add(n);
975         wc.getMap().put(s, n);
976         wc.incTotal();
977       }
978       copyExpansion(wc, cc.getContains());
979    }
980  }
981
982  private void addErrors(List<String> errs) {
983    for (String s : errs) {
984      if (!allErrors.contains(s)) {
985        allErrors.add(s);
986      }
987    }
988  }
989
990  private int copyImportContains(List<ValueSetExpansionContainsComponent> list, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filter, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly {
991    int count = 0;
992    opContext.deadCheck();
993    for (ValueSetExpansionContainsComponent c : list) {
994      c.checkNoModifiers("Imported Expansion in Code System", "expanding");
995      ValueSetExpansionContainsComponent np = addCode(dwc, c.getSystem(), c.getCode(), c.getDisplay(), vsSrc.getLanguage(), parent, null, expParams, c.getAbstract(), c.getInactive(), 
996          filter, noInactive, false, vsProps, makeCSProps(c.getExtensionString(ToolingExtensions.EXT_DEFINITION), null), null, c.getProperty(), null, c.getExtension(), exp, false);
997      if (np != null) {
998        count++;
999      }
1000      count = count + copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps, vsSrc, exp);
1001    }
1002    return count;
1003  }
1004
1005  private void includeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, boolean heirarchical, boolean noInactive, List<Extension> extensions, ValueSet valueSet) throws ETooCostly, FileNotFoundException, IOException, FHIRException, CodeSystemProviderExtension {
1006    opContext.deadCheck();
1007    inc.checkNoModifiers("Compose.include", "expanding");
1008    List<ValueSet> imports = new ArrayList<ValueSet>();
1009    for (CanonicalType imp : inc.getValueSet()) {
1010      imports.add(importValueSet(dwc, imp.getValue(), exp, expParams, noInactive, valueSet));
1011    }
1012
1013    if (!inc.hasSystem()) {
1014      if (imports.isEmpty()) // though this is not supposed to be the case
1015        return;
1016      dwc.resetTotal();
1017      ValueSet base = imports.get(0);
1018      checkCanonical(exp, base, focus);
1019      imports.remove(0);
1020      base.checkNoModifiers("Imported ValueSet", "expanding");
1021      dwc.incTotal(copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty(), base, exp));
1022    } else {
1023      CodeSystem cs = context.fetchSupplementedCodeSystem(inc.getSystem());
1024      if (ValueSetUtilities.isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) {
1025        doServerIncludeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, valueSet.getExpansion().getProperty());
1026      } else {
1027        if (cs.hasUserData("supplements.installed")) {
1028          for (String s : cs.getUserString("supplements.installed").split("\\,")) {
1029            requiredSupplements.remove(s);
1030          }
1031        }
1032        doInternalIncludeCodes(inc, exp, expParams, imports, cs, noInactive, valueSet);
1033      }
1034    }
1035  }
1036
1037  private void doServerIncludeCodes(ConceptSetComponent inc, boolean heirarchical, ValueSetExpansionComponent exp, List<ValueSet> imports, Parameters expParams, List<Extension> extensions, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException, CodeSystemProviderExtension, ETooCostly {
1038    opContext.deadCheck();
1039    CodeSystemProvider csp = CodeSystemProvider.factory(inc.getSystem());
1040    if (csp != null) {
1041      csp.includeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, vsProps);
1042      return;
1043    }
1044    
1045    ValueSetExpansionOutcome vso = context.expandVS(inc, heirarchical, noInactive);
1046    if (vso.getError() != null) {
1047      throw failTSE("Unable to expand imported value set: " + vso.getError());
1048    }
1049    ValueSet vs = vso.getValueset();
1050    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
1051      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
1052      if (!existsInParams(exp.getParameter(), "used-valueset", u)) {
1053        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
1054      }
1055    }
1056    if (vs.getExpansion().hasTotal()) {
1057      dwc.incTotal(vs.getExpansion().getTotal());
1058    } else {
1059      dwc.setNoTotal(true);
1060    }
1061    for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) {
1062      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue())) {
1063        exp.getParameter().add(p);
1064      }
1065    }
1066    for (Extension ex : vs.getExpansion().getExtension()) {
1067      if (Utilities.existsInList(ex.getUrl(), ToolingExtensions.EXT_EXP_TOOCOSTLY, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed")) {
1068        if (!ExtensionsUtils.hasExtension(extensions, ex.getUrl())) {
1069          extensions.add(ex);
1070        }
1071      }
1072    }
1073    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
1074      addCodeAndDescendents(dwc, cc, null, expParams, imports, noInactive, vsProps, vs, exp, !vs.getExpansion().hasTotal());
1075    }
1076  }
1077
1078
1079  public void doInternalIncludeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive, Resource vsSrc) throws NoTerminologyServiceException, TerminologyServiceException, FHIRException, ETooCostly {
1080    opContext.deadCheck();
1081    if (cs == null) {
1082      if (context.isNoTerminologyServer())
1083        throw failTSE("Unable to find code system " + inc.getSystem().toString());
1084      else
1085        throw failTSE("Unable to find code system " + inc.getSystem().toString());
1086    }
1087    checkCanonical(exp, cs, focus);
1088    cs.checkNoModifiers("Code System", "expanding");
1089    if (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)
1090      throw failTSE("Code system " + inc.getSystem().toString() + " is incomplete");
1091    if (cs.hasVersion() || REPORT_VERSION_ANYWAY) {
1092      UriType u = new UriType(cs.getUrl() + (cs.hasVersion() ? "|"+cs.getVersion() : ""));
1093      if (!existsInParams(exp.getParameter(), "used-codesystem", u))
1094        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-codesystem").setValue(u));
1095      if (cs.hasUserData("supplements.installed")) {
1096        for (String s : cs.getUserString("supplements.installed").split("\\,")) {
1097          u = new UriType(s);
1098          if (!existsInParams(exp.getParameter(), "used-supplement", u)) {
1099            exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-supplement").setValue(u));
1100          }
1101        }
1102      }
1103    }
1104    if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
1105      // special case - add all the code system
1106      for (ConceptDefinitionComponent def : cs.getConcept()) {
1107        addCodeAndDescendents(dwc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), null, exp);
1108      }
1109      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1110        addFragmentWarning(exp, cs);
1111      }
1112      if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
1113        addExampleWarning(exp, cs);
1114      }      
1115    }
1116
1117    if (!inc.getConcept().isEmpty()) {
1118      dwc.setCanBeHierarchy(false);
1119      for (ConceptReferenceComponent c : inc.getConcept()) {
1120        c.checkNoModifiers("Code in Value Set", "expanding");
1121        ConceptDefinitionComponent def = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null);
1122        boolean inactive = false; // default is true if we're a fragment and  
1123        boolean isAbstract = false;
1124        if (def == null) {
1125          if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1126            addFragmentWarning(exp, cs);
1127          } else if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
1128              addExampleWarning(exp, cs);
1129          } else {
1130            if (checkCodesWhenExpanding) {
1131              throw failTSE("Unable to find code '" + c.getCode() + "' in code system " + cs.getUrl());
1132            }
1133          }
1134        } else {
1135          def.checkNoModifiers("Code in Code System", "expanding");
1136          inactive = CodeSystemUtilities.isInactive(cs, def);
1137          isAbstract = CodeSystemUtilities.isNotSelectable(cs, def);
1138          addCode(dwc, inc.getSystem(), c.getCode(), !Utilities.noString(c.getDisplay()) ? c.getDisplay() : def.getDisplay(), c.hasDisplay() ? vsSrc.getLanguage() : cs.getLanguage(), null, mergeDesignations(def, convertDesignations(c.getDesignation())), 
1139              expParams, isAbstract, inactive, imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), c.getExtension(), exp, true);
1140        }
1141      }
1142    }
1143    if (inc.getFilter().size() > 0) {
1144      if (inc.getFilter().size() > 1) {
1145        dwc.setCanBeHierarchy(false); // which will be the case if we get around to supporting this
1146      }
1147      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1148        addFragmentWarning(exp, cs);
1149      }
1150      List<WorkingContext> filters = new ArrayList<>();
1151      for (int i = 1; i < inc.getFilter().size(); i++) {
1152        WorkingContext wc = new WorkingContext();
1153        filters.add(wc);
1154        processFilter(inc, exp, expParams, imports, cs, noInactive, inc.getFilter().get(i), wc, null, false);
1155      }
1156      ConceptSetFilterComponent fc = inc.getFilter().get(0);
1157      WorkingContext wc = dwc;
1158      processFilter(inc, exp, expParams, imports, cs, noInactive, fc, wc, filters, false);
1159    }
1160  }
1161
1162  private void processFilter(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive, 
1163      ConceptSetFilterComponent fc, WorkingContext wc, List<WorkingContext> filters, boolean exclude)
1164      throws ETooCostly {
1165    opContext.deadCheck();
1166    if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) {
1167      // special: all codes in the target code system under the value
1168      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1169      if (def == null)
1170        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1171      if (exclude) {
1172        excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1173      } else {
1174        addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1175      }
1176    } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISNOTA) {
1177      // special: all codes in the target code system that are not under the value
1178      ConceptDefinitionComponent defEx = getConceptForCode(cs.getConcept(), fc.getValue());
1179      if (defEx == null)
1180        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1181      for (ConceptDefinitionComponent def : cs.getConcept()) {
1182        if (exclude) {
1183          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, defEx, new AllConceptsFilter(allErrors), filters, exp);
1184        } else {
1185          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1186        }
1187      }
1188    } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) {
1189      // special: all codes in the target code system under the value
1190      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1191      if (def == null)
1192        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1193      for (ConceptDefinitionComponent c : def.getConcept())
1194        if (exclude) {
1195          excludeCodeAndDescendents(wc, cs, inc.getSystem(), c, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1196        } else {
1197          addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1198        }
1199      if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
1200        List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
1201        for (ConceptDefinitionComponent c : children) {
1202          if (exclude) {
1203            excludeCodeAndDescendents(wc, cs, inc.getSystem(), c, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1204          } else {
1205            addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1206          }
1207        }
1208      }
1209
1210    } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) {
1211      // gg; note: wtf is this: if the filter is display=v, look up the code 'v', and see if it's display is 'v'?
1212      dwc.setCanBeHierarchy(false);
1213      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1214      if (def != null) {
1215        if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) {
1216          if (def.getDisplay().contains(fc.getValue()) && passesOtherFilters(filters, cs, def.getCode())) {
1217            for (String code : getCodesForConcept(def, expParams)) {
1218              opContext.deadCheck();
1219              if (exclude) {
1220                excludeCode(wc, inc.getSystem(), code);
1221              } else {
1222                addCode(wc, inc.getSystem(), code, def.getDisplay(), cs.getLanguage(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def),
1223                  imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), null, exp, true);
1224              }
1225            }
1226          }
1227        }
1228      }
1229    } else if (CodeSystemUtilities.isDefinedProperty(cs, fc.getProperty())) {
1230      for (ConceptDefinitionComponent def : cs.getConcept()) {
1231        PropertyFilter pf = new PropertyFilter(allErrors, fc, CodeSystemUtilities.getPropertyDefinition(cs, fc.getProperty()));
1232        if (exclude) {
1233          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, pf, filters, exp);
1234        } else {
1235          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, pf, noInactive, exp.getProperty(), filters, exp);
1236        }
1237      }
1238    } else if (isKnownProperty(fc.getProperty(), cs)) {
1239      for (ConceptDefinitionComponent def : cs.getConcept()) {
1240        KnownPropertyFilter pf = new KnownPropertyFilter(allErrors, fc, fc.getProperty());
1241        if (exclude) {
1242          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, pf, filters, exp);
1243        } else {
1244          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, pf, noInactive, exp.getProperty(), filters, exp);
1245        }
1246      }
1247    } else if ("code".equals(fc.getProperty()) && fc.getOp() == FilterOperator.REGEX) {
1248      for (ConceptDefinitionComponent def : cs.getConcept()) {
1249        if (exclude) {
1250          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, new RegexFilter(allErrors, fc.getValue()), filters, exp);
1251        } else {
1252          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new RegexFilter(allErrors, fc.getValue()), noInactive, exp.getProperty(), filters, exp);
1253        }
1254      }
1255    } else {
1256      throw fail("Filter by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet");
1257    }
1258  }
1259
1260  private boolean isKnownProperty(String property, CodeSystem cs) {
1261    return Utilities.existsInList(property, "notSelectable");
1262  }
1263
1264  private List<ConceptDefinitionDesignationComponent> mergeDesignations(ConceptDefinitionComponent def,
1265      List<ConceptDefinitionDesignationComponent> list) {
1266    List<ConceptDefinitionDesignationComponent> res = new ArrayList<>();
1267    if (def != null) {
1268      res.addAll(def.getDesignation());
1269    }
1270    res.addAll(list);
1271    return res;
1272  }
1273
1274 
1275
1276  private void addFragmentWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
1277    String url = cs.getVersionedUrl();
1278    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
1279      if ("fragment".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
1280        return;
1281      }     
1282    }
1283    exp.addParameter().setName("fragment").setValue(new CanonicalType(url));
1284  }
1285
1286  private void addExampleWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
1287    String url = cs.getVersionedUrl();
1288    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
1289      if ("example".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
1290        return;
1291      }     
1292    }
1293    exp.addParameter().setName("example").setValue(new CanonicalType(url));
1294  }
1295  
1296  private List<ConceptDefinitionDesignationComponent> convertDesignations(List<ConceptReferenceDesignationComponent> list) {
1297    List<ConceptDefinitionDesignationComponent> res = new ArrayList<CodeSystem.ConceptDefinitionDesignationComponent>();
1298    for (ConceptReferenceDesignationComponent t : list) {
1299      ConceptDefinitionDesignationComponent c = new ConceptDefinitionDesignationComponent();
1300      c.setLanguage(t.getLanguage());
1301      c.setUse(t.getUse());
1302      c.setValue(t.getValue());
1303      c.getExtension().addAll(t.getExtension());
1304      res.add(c);
1305    }
1306    return res;
1307  }
1308
1309  private String key(String uri, String code) {
1310    return "{" + uri + "}" + code;
1311  }
1312
1313  private String key(ValueSetExpansionContainsComponent c) {
1314    return key(c.getSystem(), c.getCode());
1315  }
1316
1317  private FHIRException fail(String msg) {
1318    allErrors.add(msg);
1319    return new FHIRException(msg);
1320  }
1321
1322  private ETooCostly failCostly(String msg) {
1323    allErrors.add(msg);
1324    return new ETooCostly(msg);
1325  }
1326
1327  private TerminologyServiceException failTSE(String msg) {
1328    allErrors.add(msg);
1329    return new TerminologyServiceException(msg);
1330  }
1331
1332  public Collection<? extends String> getAllErrors() {
1333    return allErrors;
1334  }
1335
1336  public boolean isCheckCodesWhenExpanding() {
1337    return checkCodesWhenExpanding;
1338  }
1339
1340  public void setCheckCodesWhenExpanding(boolean checkCodesWhenExpanding) {
1341    this.checkCodesWhenExpanding = checkCodesWhenExpanding;
1342  }
1343
1344  private boolean passesOtherFilters(List<WorkingContext> otherFilters, CodeSystem cs, String code) {
1345    if (otherFilters == null) {
1346      return true;
1347    }
1348    String key = key(cs.getUrl(), code);
1349    for (WorkingContext wc : otherFilters) {
1350      if (!wc.getMap().containsKey(key)) {
1351        return false;
1352      }
1353    }
1354    return true;
1355  }
1356
1357  public boolean isDebug() {
1358    return debug;
1359  }
1360
1361  public ValueSetExpander setDebug(boolean debug) {
1362    this.debug = debug;
1363    return this;
1364  }
1365  
1366  
1367}