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) 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.getExcludeKeys().contains(s)) {
323      return null;
324    } else if (wc.getMap().containsKey(s)) {
325      wc.setCanBeHierarchy(false);
326    } else {
327      wc.getCodes().add(n);
328      wc.getMap().put(s, n);
329//      if (wc == dwc && wc.getTotal() > maxExpansionSize) {
330//        if (wc.getOffset()+wc.getCount() > 0 && wc.getTotal() > wc.getOffset()+wc.getCount()) {
331//          wc.setTotal(-1);
332//          throw new EFinished();
333//        }
334//        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, focus.getUrl(), ">" + Integer.toString(maxExpansionSize)));
335//      }
336    }
337    if (wc.isCanBeHierarchy() && parent != null) {
338      parent.getContains().add(n);
339    } else if (!wc.getRootMap().containsKey(s)) {
340      wc.getRootMap().put(s, n);
341      wc.getRoots().add(n);
342    }
343    return n;
344  }
345
346  private DataType convertToDecimal(DataType v) {
347    if (v == null) {
348      return null;
349    } 
350    if (v instanceof DecimalType) {
351      return v;
352    } 
353    if (v instanceof IntegerType) {
354      return new DecimalType(((IntegerType) v).asStringValue());
355    }
356    return null;
357  }
358
359  private boolean passesDesignationFilter(ConceptDefinitionDesignationComponent d) {
360    if (designations.isEmpty()) {
361      return true;
362    }
363    for (Token t : designations) {
364      if (t.matches(d.getUse()) || t.matchesLang(d.getLanguage())) {
365        return true;
366      }
367      for (Coding c : d.getAdditionalUse()) {
368        if (t.matches(c)) {
369          return true;
370        }
371      }
372    }
373    return false;
374  }
375
376  private ConceptDefinitionDesignationComponent findMatchingDesignation(List<ConceptDefinitionDesignationComponent> designations) {
377    if (langs == null) {
378      return null;
379    }
380    // we have a list of languages in priority order 
381    // we have a list of designations in no order 
382    // language exact match is preferred
383    // display is always preferred
384    
385    for (LanguagePreference lang : langs.getLangs()) {
386      if (lang.getValue() > 0) {
387        for (ConceptDefinitionDesignationComponent cd : designations) {
388          if (isDisplay(cd) && LanguageUtils.langsMatchExact(cd.getLanguage(), lang.getLang())) {
389            return cd;
390          }
391        }
392        for (ConceptDefinitionDesignationComponent cd : designations) {
393          if (isDisplay(cd) && LanguageUtils.langsMatch(cd.getLanguage(), lang.getLang())) {
394            return cd;
395          }
396        }
397        for (ConceptDefinitionDesignationComponent cd : designations) {
398          if (LanguageUtils.langsMatchExact(cd.getLanguage(), lang.getLang())) {
399            return cd;
400          }
401        }
402        for (ConceptDefinitionDesignationComponent cd : designations) {
403          if (LanguageUtils.langsMatch(cd.getLanguage(), lang.getLang())) {
404            return cd;
405          }
406        }
407      }
408    }
409    return null;
410  }
411
412  private boolean isDisplay(ConceptDefinitionDesignationComponent cd) {
413    return cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "display");
414  }
415
416  private boolean filterContainsCode(List<ValueSet> filters, String system, String code, ValueSetExpansionComponent exp) {
417    for (ValueSet vse : filters) {
418      checkCanonical(exp, vse, focus);
419      if (expansionContainsCode(vse.getExpansion().getContains(), system, code))
420        return true;
421    }
422    return false;
423  }
424
425  private boolean expansionContainsCode(List<ValueSetExpansionContainsComponent> contains, String system, String code) {
426    for (ValueSetExpansionContainsComponent cc : contains) {
427      if (system.equals(cc.getSystem()) && code.equals(cc.getCode()))
428        return true;
429      if (expansionContainsCode(cc.getContains(), system, code))
430        return true;
431    }
432    return false;
433  }
434
435  private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, AcceptLanguageHeader langs) {
436    for (ConceptDefinitionDesignationComponent t : list) {
437      if (LanguageUtils.langsMatchExact(langs, t.getLanguage())) {
438        return t;
439      }
440    }
441    for (ConceptDefinitionDesignationComponent t : list) {
442      if (LanguageUtils.langsMatch(langs, t.getLanguage())) {
443        return t;
444      }
445    }
446    return null;
447  }
448
449  private void addCodeAndDescendents(WorkingContext wc, ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp)  throws FHIRException, ETooCostly {
450    opContext.deadCheck();
451    focus.checkNoModifiers("Expansion.contains", "expanding");
452    ValueSetExpansionContainsComponent np = null;
453    for (String code : getCodesForConcept(focus, expParams)) {
454      ValueSetExpansionContainsComponent t = addCode(wc, focus.getSystem(), code, focus.getDisplay(), vsSrc.getLanguage(), parent, 
455           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);
456      if (np == null) {
457        np = t;
458      }
459    }
460    for (ValueSetExpansionContainsComponent c : focus.getContains())
461      addCodeAndDescendents(wc, c, np, expParams, filters, noInactive, vsProps, vsSrc, exp);
462  }
463  
464  private List<ConceptPropertyComponent> makeCSProps(String definition, List<ConceptPropertyComponent> list) {
465    List<ConceptPropertyComponent> res = new ArrayList<>();
466    if (!Utilities.noString(definition)) {
467      res.add(new ConceptPropertyComponent("definition", new StringType(definition)));
468    }
469    if (list != null) {
470      res.addAll(list);
471    }
472    return res;
473  }
474
475  private List<String> getCodesForConcept(ValueSetExpansionContainsComponent focus, Parameters expParams) {
476    List<String> codes = new ArrayList<>();
477    codes.add(focus.getCode());
478    for (org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent p : focus.getProperty()) {
479      if ("alternateCode".equals(p.getCode()) && (altCodeParams.passes(p.getExtension())) && p.getValue().isPrimitive()) {
480        codes.add(p.getValue().primitiveValue());        
481      }
482    }
483    return codes;
484  }
485
486  private List<ConceptDefinitionDesignationComponent> convert(List<ConceptReferenceDesignationComponent> designations) {
487    List<ConceptDefinitionDesignationComponent> list = new ArrayList<ConceptDefinitionDesignationComponent>();
488    for (ConceptReferenceDesignationComponent d : designations) {
489      ConceptDefinitionDesignationComponent n = new ConceptDefinitionDesignationComponent();
490      n.setLanguage(d.getLanguage());
491      n.setUse(d.getUse());
492      n.setValue(d.getValue());
493      list.add(n);
494    }
495    return list;
496  }
497
498  private void addCodeAndDescendents(WorkingContext wc, CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, 
499        ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps, List<WorkingContext> otherFilters, ValueSetExpansionComponent exp)  throws FHIRException, ETooCostly {
500    opContext.deadCheck();
501    def.checkNoModifiers("Code in Code System", "expanding");
502    if (exclusion != null) {
503      if (exclusion.getCode().equals(def.getCode()))
504        return; // excluded.
505    }
506    ValueSetExpansionContainsComponent np = null;
507    boolean abs = CodeSystemUtilities.isNotSelectable(cs, def);
508    boolean inc = CodeSystemUtilities.isInactive(cs, def);
509    boolean dep = CodeSystemUtilities.isDeprecated(cs, def, false);
510    if ((includeAbstract || !abs)  && filterFunc.includeConcept(cs, def) && passesOtherFilters(otherFilters, cs, def.getCode())) {
511      for (String code : getCodesForConcept(def, expParams)) {
512        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);
513        if (np == null) {
514          np = t;
515        }
516      }
517    }
518    for (ConceptDefinitionComponent c : def.getConcept()) {
519      addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters, exp);
520    }
521    if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
522      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
523      for (ConceptDefinitionComponent c : children)
524        addCodeAndDescendents(wc, cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps, otherFilters, exp);
525    }
526  }
527
528
529  private void excludeCodeAndDescendents(WorkingContext wc, CodeSystem cs, String system, ConceptDefinitionComponent def, Parameters expParams, List<ValueSet> filters, 
530      ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, List<WorkingContext> otherFilters, ValueSetExpansionComponent exp)  throws FHIRException, ETooCostly {
531    opContext.deadCheck();
532    def.checkNoModifiers("Code in Code System", "expanding");
533    if (exclusion != null) {
534      if (exclusion.getCode().equals(def.getCode()))
535        return; // excluded.
536    }
537    boolean abs = CodeSystemUtilities.isNotSelectable(cs, def);
538    if ((includeAbstract || !abs)  && filterFunc.includeConcept(cs, def) && passesOtherFilters(otherFilters, cs, def.getCode())) {
539      for (String code : getCodesForConcept(def, expParams)) {
540        if (!(filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code, exp)))
541          excludeCode(wc, system, code);
542      }
543    }
544    for (ConceptDefinitionComponent c : def.getConcept()) {
545      excludeCodeAndDescendents(wc, cs, system, c, expParams, filters, exclusion, filterFunc, otherFilters, exp);
546    }
547    if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
548      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
549      for (ConceptDefinitionComponent c : children)
550        excludeCodeAndDescendents(wc, cs, system, c, expParams, filters, exclusion, filterFunc, otherFilters, exp);
551    }
552  }
553
554
555  private List<String> getCodesForConcept(ConceptDefinitionComponent focus, Parameters expParams) {
556    List<String> codes = new ArrayList<>();
557    codes.add(focus.getCode());
558    for (ConceptPropertyComponent p : focus.getProperty()) {
559      if ("alternateCode".equals(p.getCode()) && (altCodeParams.passes(p.getExtension())) && p.getValue().isPrimitive()) {
560        codes.add(p.getValue().primitiveValue());        
561      }
562    }
563    return codes;
564  }
565
566  private static boolean hasUse(ConceptPropertyComponent p, List<String> uses) {
567    for (Extension ext : p.getExtensionsByUrl(ToolingExtensions.EXT_CS_ALTERNATE_USE)) {
568      if (ext.hasValueCoding() && Utilities.existsInList(ext.getValueCoding().getCode(), uses)) {
569        return true;
570      }
571    }
572    return false;
573  }
574
575  
576
577  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 {
578    if (expand != null) {
579      if (expand.getContains().size() > maxExpansionSize)
580        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, vsSrc.getUrl(), ">" + Integer.toString(expand.getContains().size())));
581      for (ValueSetExpansionParameterComponent p : expand.getParameter()) {
582        if (!existsInParams(params, p.getName(), p.getValue()))
583          params.add(p);
584      }
585      
586      copyImportContains(expand.getContains(), null, expParams, filters, noInactive, vsProps, vsSrc, exp);
587    }
588  }
589
590  private void excludeCode(WorkingContext wc, String theSystem, String theCode) {
591    ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
592    n.setSystem(theSystem);
593    n.setCode(theCode);
594    String s = key(n);
595    wc.getExcludeKeys().add(s);
596  }
597
598  private void excludeCodes(WorkingContext wc, ConceptSetComponent exc, Parameters expParams, ValueSetExpansionComponent exp, ValueSet vs) throws FHIRException, FileNotFoundException, ETooCostly, IOException {
599    opContext.deadCheck();
600    exc.checkNoModifiers("Compose.exclude", "expanding");
601    if (exc.hasSystem() && exc.getConcept().size() == 0 && exc.getFilter().size() == 0) {
602      wc.getExcludeSystems().add(exc.getSystem());
603    }
604
605    for (UriType imp : exc.getValueSet()) {
606      excludeCodes(wc, importValueSetForExclude(wc, imp.getValue(), exp, expParams, false, vs).getExpansion());
607    }
608    
609    CodeSystem cs = context.fetchSupplementedCodeSystem(exc.getSystem());
610    if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(exc.getSystem(), opContext.getOptions().getFhirVersion())) {
611      ValueSetExpansionOutcome vse = context.expandVS(exc, false, false);
612      ValueSet valueset = vse.getValueset();
613      if (valueset == null)
614        throw failTSE("Error Expanding ValueSet: "+vse.getError());
615      excludeCodes(wc, valueset.getExpansion());
616      return;
617    }
618
619    for (ConceptReferenceComponent c : exc.getConcept()) {
620      excludeCode(wc, exc.getSystem(), c.getCode());
621    }
622
623    if (exc.getFilter().size() > 0) {
624      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
625        addFragmentWarning(exp, cs);
626      }
627      List<WorkingContext> filters = new ArrayList<>();
628      for (int i = 1; i < exc.getFilter().size(); i++) {
629        WorkingContext wc1 = new WorkingContext();
630        filters.add(wc1);
631        processFilter(exc, exp, expParams, null, cs, false, exc.getFilter().get(i), wc1, null, true);
632      }
633      ConceptSetFilterComponent fc = exc.getFilter().get(0);
634      WorkingContext wc1 = dwc;
635      processFilter(exc, exp, expParams, null, cs, false, fc, wc1, filters, true);
636    }
637  }
638
639  private void excludeCodes(WorkingContext wc, ValueSetExpansionComponent expand) {
640    opContext.deadCheck();
641    for (ValueSetExpansionContainsComponent c : expand.getContains()) {
642      excludeCode(wc, c.getSystem(), c.getCode());
643    }
644  }
645
646  private boolean existsInParams(List<ValueSetExpansionParameterComponent> params, String name, DataType value) {
647    for (ValueSetExpansionParameterComponent p : params) {
648      if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) {
649        return true;
650      }
651    }
652    return false;
653  }
654
655  public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) {
656    
657    allErrors.clear();
658    try {
659      opContext.seeContext(source.getVersionedUrl());
660      
661      return expandInternal(source, expParams);
662    } catch (NoTerminologyServiceException e) {
663      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
664      // that might fail too, but it might not, later.
665      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.NOSERVICE, allErrors, false);
666    } catch (CodeSystemProviderExtension e) {
667      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
668      // that might fail too, but it might not, later.
669      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.INTERNAL_ERROR, allErrors, false);
670    } catch (TerminologyServiceProtectionException e) {
671      if (opContext.isOriginal()) {
672        return new ValueSetExpansionOutcome(e.getMessage(), e.getError(), allErrors, false);
673      } else {
674        throw e;
675      }
676    } catch (ETooCostly e) {
677      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.TOO_COSTLY, allErrors, false);
678    } catch (Exception e) {
679      if (debug) {
680        e.printStackTrace();
681      }
682      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.UNKNOWN, allErrors, e instanceof EFhirClientException || e instanceof TerminologyServiceException);
683    }
684  }
685  
686  public ValueSetExpansionOutcome expandInternal(ValueSet source, Parameters expParams) throws FHIRException, FileNotFoundException, ETooCostly, IOException, CodeSystemProviderExtension {
687      return doExpand(source, expParams);
688  }
689
690  private void processParameter(String name, DataType value) {
691    if (Utilities.existsInList(name, "includeDesignations", "excludeNested", "activeOnly", "offset", "count")) {
692      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
693      focus.getExpansion().addParameter().setName(name).setValue(value);
694    }
695    if ("displayLanguage".equals(name)) {
696      this.langs = new AcceptLanguageHeader(value.primitiveValue(), true);
697      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
698      focus.getExpansion().addParameter().setName(name).setValue(new CodeType(value.primitiveValue()));
699    }
700    if ("designation".equals(name)) {
701      String[] v = value.primitiveValue().split("\\|");
702      if (v.length != 2 || !Utilities.isAbsoluteUrl(v[0]) || Utilities.noString(v[1])) {
703        throw new NoTerminologyServiceException("Unable to understand designation parameter "+value.primitiveValue());
704      }
705      this.designations.add(new Token(v[0], v[1]));
706      focus.getExpansion().addParameter().setName(name).setValue(new StringType(value.primitiveValue()));
707    }
708    if ("offset".equals(name) && value instanceof IntegerType) {
709      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
710      focus.getExpansion().addParameter().setName(name).setValue(value);
711      dwc.setOffsetParam(((IntegerType) value).getValue());
712      if (dwc.getOffsetParam() < 0) {
713        dwc.setOffsetParam(0);
714      }
715    }
716    if ("count".equals(name)) {
717      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
718      focus.getExpansion().addParameter().setName(name).setValue(value);
719      dwc.setCountParam(((IntegerType) value).getValue());
720      if (dwc.getCountParam() < 0) {
721        dwc.setCountParam(0);
722      }
723    }
724  }
725  
726  public ValueSetExpansionOutcome doExpand(ValueSet source, Parameters expParams) throws FHIRException, ETooCostly, FileNotFoundException, IOException, CodeSystemProviderExtension {
727    if (expParams == null)
728      expParams = makeDefaultExpansion();
729    altCodeParams.seeParameters(expParams);
730    altCodeParams.seeValueSet(source);
731    
732    source.checkNoModifiers("ValueSet", "expanding");
733    focus = source.copy();
734    focus.setIdBase(null);
735    focus.setExpansion(new ValueSet.ValueSetExpansionComponent());
736    focus.getExpansion().setTimestampElement(DateTimeType.now());
737    focus.getExpansion().setIdentifier(Factory.createUUID()); 
738    checkCanonical(focus.getExpansion(), focus, focus);
739    for (Extension ext : focus.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) {
740      processParameter(ext.getExtensionString("name"), ext.getExtensionByUrl("value").getValue());
741    }
742    for (ParametersParameterComponent p : expParams.getParameter()) {
743      processParameter(p.getName(), p.getValue());
744    }
745    for (Extension s : focus.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) {
746      requiredSupplements.add(s.getValue().primitiveValue());
747    }
748    if (langs == null && focus.hasLanguage()) {
749      langs = new AcceptLanguageHeader(focus.getLanguage(), true);
750    }
751
752    try {
753      if (source.hasCompose()) {
754//        ExtensionsUtils.stripExtensions(focus.getCompose()); - disabled 23/05/2023 GDG - why was this ever thought to be a good idea?
755        handleCompose(source.getCompose(), focus.getExpansion(), expParams, source.getUrl(), focus.getExpansion().getExtension(), source);
756      }
757    } catch (EFinished e) {
758      // nothing - we intended to trap this here
759    }
760
761    if (dwc.getCount() > maxExpansionSize && dwc.getOffsetParam() + dwc.getCountParam() == 0) {
762      if (dwc.isNoTotal()) {
763        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY, focus.getVersionedUrl(), ">" + MessageFormat.format("{0,number,#}", maxExpansionSize)));        
764      } else {
765        throw failCostly(context.formatMessage(I18nConstants.VALUESET_TOO_COSTLY_COUNT, focus.getVersionedUrl(), ">" + MessageFormat.format("{0,number,#}", maxExpansionSize), MessageFormat.format("{0,number,#}", dwc.getCount())));
766      }
767    } else if (dwc.isCanBeHierarchy() && ((dwc.getCountParam() == 0) || dwc.getCountParam() > dwc.getCodes().size())) {
768      for (ValueSetExpansionContainsComponent c : dwc.getRoots()) {
769        focus.getExpansion().getContains().add(c);
770      }
771    } else {
772      int i = 0;
773      int cc = 0;
774      for (ValueSetExpansionContainsComponent c : dwc.getCodes()) {
775        c.getContains().clear(); // make sure any hierarchy is wiped
776        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
777          if (dwc.getOffsetParam() == 0 || i >= dwc.getOffsetParam()) {
778            focus.getExpansion().getContains().add(c);
779            cc++;
780            if (cc == dwc.getCountParam()) {
781              break;
782            }
783          }
784          i++;
785        }
786      }
787    }
788
789    if (dwc.hasOffsetParam()) {
790      focus.getExpansion().setOffset(dwc.getOffsetParam());
791    }
792    if (!dwc.isNoTotal()) {
793      focus.getExpansion().setTotal(dwc.getStatedTotal());
794    }
795    if (!requiredSupplements.isEmpty()) {      
796      return new ValueSetExpansionOutcome(context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements)), TerminologyServiceErrorClass.BUSINESS_RULE, allErrors, false);
797    }
798    if (!expParams.hasParameter("includeDefinition") || !expParams.getParameterBool("includeDefinition")) {
799      focus.setCompose(null);
800      focus.getExtension().clear();
801      focus.setPublisher(null);
802      focus.setDescription(null);
803      focus.setPurpose(null);
804      focus.getContact().clear();
805      focus.setCopyright(null);
806      focus.setText(null);
807    }
808    return new ValueSetExpansionOutcome(focus);
809  }
810
811
812  private Parameters makeDefaultExpansion() {
813    Parameters res = new Parameters();
814    res.addParameter("excludeNested", true);
815    res.addParameter("includeDesignations", false);
816    return res;
817  }
818
819  private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> clist, String code) {
820    for (ConceptDefinitionComponent c : clist) {
821      if (code.equals(c.getCode()))
822        return c;
823      ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code);
824      if (v != null)
825        return v;
826    }
827    return null;
828  }
829
830  private void handleCompose(ValueSetComposeComponent compose, ValueSetExpansionComponent exp, Parameters expParams, String ctxt, List<Extension> extensions, ValueSet valueSet)
831      throws ETooCostly, FileNotFoundException, IOException, FHIRException, CodeSystemProviderExtension {
832    compose.checkNoModifiers("ValueSet.compose", "expanding");
833    // Exclude comes first because we build up a map of things to exclude
834    for (ConceptSetComponent inc : compose.getExclude()) {
835      excludeCodes(dwc, inc, expParams, exp, valueSet);
836    }
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      // because if there's no total, we can't know if we got everything
903      dwc.setNoTotal(true);
904    }
905    for (ValueSetExpansionParameterComponent p : evs.getParameter()) {
906      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue()))
907        exp.getParameter().add(p);
908    }
909    if (isValueSetUnionImports(valueSet)) {
910      copyExpansion(wc, evs.getContains());
911    }
912    wc.setCanBeHierarchy(false); // if we're importing a value set, we have to be combining, so we won't try for a hierarchy
913    return vso.getValueset();
914  }
915  
916
917  private ValueSet importValueSetForExclude(WorkingContext wc, String value, ValueSetExpansionComponent exp, Parameters expParams, boolean noInactive, ValueSet valueSet) throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError {
918    if (value == null)
919      throw fail("unable to find value set with no identity");
920    ValueSet vs = context.findTxResource(ValueSet.class, value, valueSet);
921    if (vs == null) {
922      if (context.fetchResource(CodeSystem.class, value, valueSet) != null) {
923        throw fail("Cannot include value set "+value+" because it's actually a code system");
924      } else {
925        throw fail("Unable to find imported value set " + value);
926      }
927    }
928    checkCanonical(exp, vs, focus);
929    if (noInactive) {
930      expParams = expParams.copy();
931      expParams.addParameter("activeOnly", true);
932    }
933    ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams);
934    if (vso.getError() != null) {
935      addErrors(vso.getAllErrors());
936      throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError());
937    }
938    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
939      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
940      if (!existsInParams(exp.getParameter(), "used-valueset", u))
941        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
942    }
943    for (Extension ex : vso.getValueset().getExpansion().getExtension()) {
944      if (ex.getUrl().equals(ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
945        throw fail("Unable to expand imported value set "+vs.getUrl()+" for exclude: too costly");
946      } 
947    }
948    return vso.getValueset();
949  }
950  
951  protected boolean isValueSetUnionImports(ValueSet valueSet) {
952    PackageInformation p = valueSet.getSourcePackage();
953    if (p != null) {
954      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
955    } else {
956      return false;
957    }
958  }
959
960  public void copyExpansion(WorkingContext wc,List<ValueSetExpansionContainsComponent> list) {
961    opContext.deadCheck();
962    for (ValueSetExpansionContainsComponent cc : list) {
963       ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
964       n.setSystem(cc.getSystem());
965       n.setCode(cc.getCode());
966       n.setAbstract(cc.getAbstract());
967       n.setInactive(cc.getInactive());
968       n.setDisplay(cc.getDisplay());
969       n.getDesignation().addAll(cc.getDesignation());
970
971       String s = key(n);
972       if (!wc.getMap().containsKey(s) && !wc.getExcludeKeys().contains(s)) {
973         wc.getCodes().add(n);
974         wc.getMap().put(s, n);
975       }
976       copyExpansion(wc, cc.getContains());
977    }
978  }
979
980  private void addErrors(List<String> errs) {
981    for (String s : errs) {
982      if (!allErrors.contains(s)) {
983        allErrors.add(s);
984      }
985    }
986  }
987
988  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 {
989    int count = 0;
990    opContext.deadCheck();
991    for (ValueSetExpansionContainsComponent c : list) {
992      c.checkNoModifiers("Imported Expansion in Code System", "expanding");
993      ValueSetExpansionContainsComponent np = addCode(dwc, c.getSystem(), c.getCode(), c.getDisplay(), vsSrc.getLanguage(), parent, null, expParams, c.getAbstract(), c.getInactive(), 
994          filter, noInactive, false, vsProps, makeCSProps(c.getExtensionString(ToolingExtensions.EXT_DEFINITION), null), null, c.getProperty(), null, c.getExtension(), exp);
995      if (np != null) {
996        count++;
997      }
998      count = count + copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps, vsSrc, exp);
999    }
1000    return count;
1001  }
1002
1003  private void includeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, boolean heirarchical, boolean noInactive, List<Extension> extensions, ValueSet valueSet) throws ETooCostly, FileNotFoundException, IOException, FHIRException, CodeSystemProviderExtension {
1004    opContext.deadCheck();
1005    inc.checkNoModifiers("Compose.include", "expanding");
1006    List<ValueSet> imports = new ArrayList<ValueSet>();
1007    for (CanonicalType imp : inc.getValueSet()) {
1008      imports.add(importValueSet(dwc, imp.getValue(), exp, expParams, noInactive, valueSet));
1009    }
1010
1011    if (!inc.hasSystem()) {
1012      if (imports.isEmpty()) // though this is not supposed to be the case
1013        return;
1014      ValueSet base = imports.get(0);
1015      checkCanonical(exp, base, focus);
1016      imports.remove(0);
1017      base.checkNoModifiers("Imported ValueSet", "expanding");
1018      copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty(), base, exp);
1019    } else {
1020      CodeSystem cs = context.fetchSupplementedCodeSystem(inc.getSystem());
1021      if (ValueSetUtilities.isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) {
1022        doServerIncludeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, valueSet.getExpansion().getProperty());
1023      } else {
1024        if (cs.hasUserData("supplements.installed")) {
1025          for (String s : cs.getUserString("supplements.installed").split("\\,")) {
1026            requiredSupplements.remove(s);
1027          }
1028        }
1029        doInternalIncludeCodes(inc, exp, expParams, imports, cs, noInactive, valueSet);
1030      }
1031    }
1032  }
1033
1034  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 {
1035    opContext.deadCheck();
1036    CodeSystemProvider csp = CodeSystemProvider.factory(inc.getSystem());
1037    if (csp != null) {
1038      csp.includeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, vsProps);
1039      return;
1040    }
1041    
1042    ValueSetExpansionOutcome vso = context.expandVS(inc, heirarchical, noInactive);
1043    if (vso.getError() != null) {
1044      throw failTSE("Unable to expand imported value set: " + vso.getError());
1045    }
1046    ValueSet vs = vso.getValueset();
1047    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
1048      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
1049      if (!existsInParams(exp.getParameter(), "used-valueset", u)) {
1050        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
1051      }
1052    }
1053    if (vs.getExpansion().hasTotal()) {
1054      // 0 for now... dwc.incExtraCount(!vs.getExpansion().getTotal());
1055    } else {
1056      dwc.setNoTotal(true);
1057    }
1058    for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) {
1059      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue())) {
1060        exp.getParameter().add(p);
1061      }
1062    }
1063    for (Extension ex : vs.getExpansion().getExtension()) {
1064      if (Utilities.existsInList(ex.getUrl(), ToolingExtensions.EXT_EXP_TOOCOSTLY, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed")) {
1065        if (!ExtensionsUtils.hasExtension(extensions, ex.getUrl())) {
1066          extensions.add(ex);
1067        }
1068      }
1069    }
1070    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
1071      addCodeAndDescendents(dwc, cc, null, expParams, imports, noInactive, vsProps, vs, exp);
1072    }
1073  }
1074
1075
1076  public void doInternalIncludeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive, Resource vsSrc) throws NoTerminologyServiceException, TerminologyServiceException, FHIRException, ETooCostly {
1077    opContext.deadCheck();
1078    if (cs == null) {
1079      if (context.isNoTerminologyServer())
1080        throw failTSE("Unable to find code system " + inc.getSystem().toString());
1081      else
1082        throw failTSE("Unable to find code system " + inc.getSystem().toString());
1083    }
1084    checkCanonical(exp, cs, focus);
1085    cs.checkNoModifiers("Code System", "expanding");
1086    if (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)
1087      throw failTSE("Code system " + inc.getSystem().toString() + " is incomplete");
1088    if (cs.hasVersion() || REPORT_VERSION_ANYWAY) {
1089      UriType u = new UriType(cs.getUrl() + (cs.hasVersion() ? "|"+cs.getVersion() : ""));
1090      if (!existsInParams(exp.getParameter(), "used-codesystem", u))
1091        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-codesystem").setValue(u));
1092      if (cs.hasUserData("supplements.installed")) {
1093        for (String s : cs.getUserString("supplements.installed").split("\\,")) {
1094          u = new UriType(s);
1095          if (!existsInParams(exp.getParameter(), "used-supplement", u)) {
1096            exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-supplement").setValue(u));
1097          }
1098        }
1099      }
1100    }
1101    if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
1102      // special case - add all the code system
1103      for (ConceptDefinitionComponent def : cs.getConcept()) {
1104        addCodeAndDescendents(dwc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), null, exp);
1105      }
1106      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1107        addFragmentWarning(exp, cs);
1108      }
1109      if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
1110        addExampleWarning(exp, cs);
1111      }      
1112    }
1113
1114    if (!inc.getConcept().isEmpty()) {
1115      dwc.setCanBeHierarchy(false);
1116      for (ConceptReferenceComponent c : inc.getConcept()) {
1117        c.checkNoModifiers("Code in Value Set", "expanding");
1118        ConceptDefinitionComponent def = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null);
1119        boolean inactive = false; // default is true if we're a fragment and  
1120        boolean isAbstract = false;
1121        if (def == null) {
1122          if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1123            addFragmentWarning(exp, cs);
1124          } else if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
1125              addExampleWarning(exp, cs);
1126          } else {
1127            if (checkCodesWhenExpanding) {
1128              throw failTSE("Unable to find code '" + c.getCode() + "' in code system " + cs.getUrl());
1129            }
1130          }
1131        } else {
1132          def.checkNoModifiers("Code in Code System", "expanding");
1133          inactive = CodeSystemUtilities.isInactive(cs, def);
1134          isAbstract = CodeSystemUtilities.isNotSelectable(cs, def);
1135          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())), 
1136              expParams, isAbstract, inactive, imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), c.getExtension(), exp);
1137        }
1138      }
1139    }
1140    if (inc.getFilter().size() > 0) {
1141      if (inc.getFilter().size() > 1) {
1142        dwc.setCanBeHierarchy(false); // which will be the case if we get around to supporting this
1143      }
1144      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1145        addFragmentWarning(exp, cs);
1146      }
1147      List<WorkingContext> filters = new ArrayList<>();
1148      for (int i = 1; i < inc.getFilter().size(); i++) {
1149        WorkingContext wc = new WorkingContext();
1150        filters.add(wc);
1151        processFilter(inc, exp, expParams, imports, cs, noInactive, inc.getFilter().get(i), wc, null, false);
1152      }
1153      ConceptSetFilterComponent fc = inc.getFilter().get(0);
1154      WorkingContext wc = dwc;
1155      processFilter(inc, exp, expParams, imports, cs, noInactive, fc, wc, filters, false);
1156    }
1157  }
1158
1159  private void processFilter(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive, 
1160      ConceptSetFilterComponent fc, WorkingContext wc, List<WorkingContext> filters, boolean exclude)
1161      throws ETooCostly {
1162    opContext.deadCheck();
1163    if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) {
1164      // special: all codes in the target code system under the value
1165      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1166      if (def == null)
1167        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1168      if (exclude) {
1169        excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1170      } else {
1171        addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1172      }
1173    } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISNOTA) {
1174      // special: all codes in the target code system that are not under the value
1175      ConceptDefinitionComponent defEx = getConceptForCode(cs.getConcept(), fc.getValue());
1176      if (defEx == null)
1177        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1178      for (ConceptDefinitionComponent def : cs.getConcept()) {
1179        if (exclude) {
1180          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, defEx, new AllConceptsFilter(allErrors), filters, exp);
1181        } else {
1182          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1183        }
1184      }
1185    } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) {
1186      // special: all codes in the target code system under the value
1187      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1188      if (def == null)
1189        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1190      for (ConceptDefinitionComponent c : def.getConcept())
1191        if (exclude) {
1192          excludeCodeAndDescendents(wc, cs, inc.getSystem(), c, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1193        } else {
1194          addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1195        }
1196      if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
1197        List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
1198        for (ConceptDefinitionComponent c : children) {
1199          if (exclude) {
1200            excludeCodeAndDescendents(wc, cs, inc.getSystem(), c, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1201          } else {
1202            addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1203          }
1204        }
1205      }
1206
1207    } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) {
1208      // gg; note: wtf is this: if the filter is display=v, look up the code 'v', and see if it's display is 'v'?
1209      dwc.setCanBeHierarchy(false);
1210      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1211      if (def != null) {
1212        if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) {
1213          if (def.getDisplay().contains(fc.getValue()) && passesOtherFilters(filters, cs, def.getCode())) {
1214            for (String code : getCodesForConcept(def, expParams)) {
1215              opContext.deadCheck();
1216              if (exclude) {
1217                excludeCode(wc, inc.getSystem(), code);
1218              } else {
1219                addCode(wc, inc.getSystem(), code, def.getDisplay(), cs.getLanguage(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def),
1220                  imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), null, exp);
1221              }
1222            }
1223          }
1224        }
1225      }
1226    } else if (CodeSystemUtilities.isDefinedProperty(cs, fc.getProperty())) {
1227      for (ConceptDefinitionComponent def : cs.getConcept()) {
1228        PropertyFilter pf = new PropertyFilter(allErrors, fc, CodeSystemUtilities.getPropertyDefinition(cs, fc.getProperty()));
1229        if (exclude) {
1230          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, pf, filters, exp);
1231        } else {
1232          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, pf, noInactive, exp.getProperty(), filters, exp);
1233        }
1234      }
1235    } else if (isKnownProperty(fc.getProperty(), cs)) {
1236      for (ConceptDefinitionComponent def : cs.getConcept()) {
1237        KnownPropertyFilter pf = new KnownPropertyFilter(allErrors, fc, fc.getProperty());
1238        if (exclude) {
1239          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, pf, filters, exp);
1240        } else {
1241          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, pf, noInactive, exp.getProperty(), filters, exp);
1242        }
1243      }
1244    } else if ("code".equals(fc.getProperty()) && fc.getOp() == FilterOperator.REGEX) {
1245      for (ConceptDefinitionComponent def : cs.getConcept()) {
1246        if (exclude) {
1247          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, new RegexFilter(allErrors, fc.getValue()), filters, exp);
1248        } else {
1249          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new RegexFilter(allErrors, fc.getValue()), noInactive, exp.getProperty(), filters, exp);
1250        }
1251      }
1252    } else {
1253      throw fail("Filter by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet");
1254    }
1255  }
1256
1257  private boolean isKnownProperty(String property, CodeSystem cs) {
1258    return Utilities.existsInList(property, "notSelectable");
1259  }
1260
1261  private List<ConceptDefinitionDesignationComponent> mergeDesignations(ConceptDefinitionComponent def,
1262      List<ConceptDefinitionDesignationComponent> list) {
1263    List<ConceptDefinitionDesignationComponent> res = new ArrayList<>();
1264    if (def != null) {
1265      res.addAll(def.getDesignation());
1266    }
1267    res.addAll(list);
1268    return res;
1269  }
1270
1271 
1272
1273  private void addFragmentWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
1274    String url = cs.getVersionedUrl();
1275    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
1276      if ("fragment".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
1277        return;
1278      }     
1279    }
1280    exp.addParameter().setName("fragment").setValue(new CanonicalType(url));
1281  }
1282
1283  private void addExampleWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
1284    String url = cs.getVersionedUrl();
1285    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
1286      if ("example".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
1287        return;
1288      }     
1289    }
1290    exp.addParameter().setName("example").setValue(new CanonicalType(url));
1291  }
1292  
1293  private List<ConceptDefinitionDesignationComponent> convertDesignations(List<ConceptReferenceDesignationComponent> list) {
1294    List<ConceptDefinitionDesignationComponent> res = new ArrayList<CodeSystem.ConceptDefinitionDesignationComponent>();
1295    for (ConceptReferenceDesignationComponent t : list) {
1296      ConceptDefinitionDesignationComponent c = new ConceptDefinitionDesignationComponent();
1297      c.setLanguage(t.getLanguage());
1298      c.setUse(t.getUse());
1299      c.setValue(t.getValue());
1300      c.getExtension().addAll(t.getExtension());
1301      res.add(c);
1302    }
1303    return res;
1304  }
1305
1306  private String key(String uri, String code) {
1307    return "{" + uri + "}" + code;
1308  }
1309
1310  private String key(ValueSetExpansionContainsComponent c) {
1311    return key(c.getSystem(), c.getCode());
1312  }
1313
1314  private FHIRException fail(String msg) {
1315    allErrors.add(msg);
1316    return new FHIRException(msg);
1317  }
1318
1319  private ETooCostly failCostly(String msg) {
1320    allErrors.add(msg);
1321    return new ETooCostly(msg);
1322  }
1323
1324  private TerminologyServiceException failTSE(String msg) {
1325    allErrors.add(msg);
1326    return new TerminologyServiceException(msg);
1327  }
1328
1329  public Collection<? extends String> getAllErrors() {
1330    return allErrors;
1331  }
1332
1333  public boolean isCheckCodesWhenExpanding() {
1334    return checkCodesWhenExpanding;
1335  }
1336
1337  public void setCheckCodesWhenExpanding(boolean checkCodesWhenExpanding) {
1338    this.checkCodesWhenExpanding = checkCodesWhenExpanding;
1339  }
1340
1341  private boolean passesOtherFilters(List<WorkingContext> otherFilters, CodeSystem cs, String code) {
1342    if (otherFilters == null) {
1343      return true;
1344    }
1345    String key = key(cs.getUrl(), code);
1346    for (WorkingContext wc : otherFilters) {
1347      if (!wc.getMap().containsKey(key)) {
1348        return false;
1349      }
1350    }
1351    return true;
1352  }
1353
1354  public boolean isDebug() {
1355    return debug;
1356  }
1357
1358  public ValueSetExpander setDebug(boolean debug) {
1359    this.debug = debug;
1360    return this;
1361  }
1362  
1363  
1364}