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("addCode"+code);
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("addCodeAndDescendents");
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("addCodeAndDescendents");
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("excludeCodeAndDescendents");
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("excludeCodes");
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    if (exc.hasSystem()) {
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
641  private void excludeCodes(WorkingContext wc, ValueSetExpansionComponent expand) {
642    opContext.deadCheck("excludeCodes");
643    for (ValueSetExpansionContainsComponent c : expand.getContains()) {
644      excludeCode(wc, c.getSystem(), c.getCode());
645    }
646  }
647
648  private boolean existsInParams(List<ValueSetExpansionParameterComponent> params, String name, DataType value) {
649    for (ValueSetExpansionParameterComponent p : params) {
650      if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) {
651        return true;
652      }
653    }
654    return false;
655  }
656
657  public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) {
658    
659    allErrors.clear();
660    try {
661      opContext.seeContext(source.getVersionedUrl());
662      
663      return expandInternal(source, expParams);
664    } catch (NoTerminologyServiceException e) {
665      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
666      // that might fail too, but it might not, later.
667      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.NOSERVICE, allErrors, false);
668    } catch (CodeSystemProviderExtension e) {
669      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
670      // that might fail too, but it might not, later.
671      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.INTERNAL_ERROR, allErrors, false);
672    } catch (TerminologyServiceProtectionException e) {
673      if (opContext.isOriginal()) {
674        return new ValueSetExpansionOutcome(e.getMessage(), e.getError(), allErrors, false);
675      } else {
676        throw e;
677      }
678    } catch (ETooCostly e) {
679      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.TOO_COSTLY, allErrors, false);
680    } catch (Exception e) {
681      if (debug) {
682        e.printStackTrace();
683      }
684      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.UNKNOWN, allErrors, e instanceof EFhirClientException || e instanceof TerminologyServiceException);
685    }
686  }
687  
688  public ValueSetExpansionOutcome expandInternal(ValueSet source, Parameters expParams) throws FHIRException, FileNotFoundException, ETooCostly, IOException, CodeSystemProviderExtension {
689      return doExpand(source, expParams);
690  }
691
692  private void processParameter(String name, DataType value) {
693    if (Utilities.existsInList(name, "includeDesignations", "excludeNested", "activeOnly", "offset", "count")) {
694      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
695      focus.getExpansion().addParameter().setName(name).setValue(value);
696    }
697    if ("displayLanguage".equals(name)) {
698      this.langs = new AcceptLanguageHeader(value.primitiveValue(), true);
699      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
700      focus.getExpansion().addParameter().setName(name).setValue(new CodeType(value.primitiveValue()));
701    }
702    if ("designation".equals(name)) {
703      String[] v = value.primitiveValue().split("\\|");
704      if (v.length != 2 || !Utilities.isAbsoluteUrl(v[0]) || Utilities.noString(v[1])) {
705        throw new NoTerminologyServiceException("Unable to understand designation parameter "+value.primitiveValue());
706      }
707      this.designations.add(new Token(v[0], v[1]));
708      focus.getExpansion().addParameter().setName(name).setValue(new StringType(value.primitiveValue()));
709    }
710    if ("offset".equals(name) && value instanceof IntegerType) {
711      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
712      focus.getExpansion().addParameter().setName(name).setValue(value);
713      dwc.setOffsetParam(((IntegerType) value).getValue());
714      if (dwc.getOffsetParam() < 0) {
715        dwc.setOffsetParam(0);
716      }
717    }
718    if ("count".equals(name)) {
719      focus.getExpansion().getParameter().removeIf(p -> p.getName().equals(name));
720      focus.getExpansion().addParameter().setName(name).setValue(value);
721      dwc.setCountParam(((IntegerType) value).getValue());
722      if (dwc.getCountParam() < 0) {
723        dwc.setCountParam(0);
724      }
725    }
726  }
727  
728  public ValueSetExpansionOutcome doExpand(ValueSet source, Parameters expParams) throws FHIRException, ETooCostly, FileNotFoundException, IOException, CodeSystemProviderExtension {
729    if (expParams == null)
730      expParams = makeDefaultExpansion();
731    altCodeParams.seeParameters(expParams);
732    altCodeParams.seeValueSet(source);
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.getCount() > 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.getCount())));
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.getStatedTotal());
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    }
838    dwc.setCanBeHierarchy(!expParams.getParameterBool("excludeNested") && dwc.getExcludeKeys().isEmpty() && dwc.getExcludeSystems().isEmpty() && dwc.getOffsetParam() == 0);
839    includeAbstract = !expParams.getParameterBool("excludeNotForUI");
840    boolean first = true;
841    for (ConceptSetComponent inc : compose.getInclude()) {
842      if (first == true)
843        first = false;
844      else
845        dwc.setCanBeHierarchy(false);
846      includeCodes(inc, exp, expParams, dwc.isCanBeHierarchy(), compose.hasInactive() ? !compose.getInactive() : checkNoInActiveFromParam(expParams), extensions, valueSet);
847    }
848  }
849
850  /**
851   * returns true if activeOnly = true 
852   * @param expParams
853   * @return
854   */
855  private boolean checkNoInActiveFromParam(Parameters expParams) {
856    for (ParametersParameterComponent p : expParams.getParameter()) {
857      if (p.getName().equals("activeOnly")) {
858        return p.getValueBooleanType().getValue();
859      }
860    }
861    return false;
862  }
863
864  private ValueSet importValueSet(WorkingContext wc, String value, ValueSetExpansionComponent exp, Parameters expParams, boolean noInactive, ValueSet valueSet) throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError {
865    if (value == null)
866      throw fail("unable to find value set with no identity");
867    ValueSet vs = context.findTxResource(ValueSet.class, value, valueSet);
868    if (vs == null) {
869      if (context.fetchResource(CodeSystem.class, value, valueSet) != null) {
870        throw fail("Cannot include value set "+value+" because it's actually a code system");
871      } else {
872        throw fail("Unable to find imported value set " + value);
873      }
874    }
875    checkCanonical(exp, vs, focus);
876    if (noInactive) {
877      expParams = expParams.copy();
878      expParams.addParameter("activeOnly", true);
879    }
880    ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams);
881    if (vso.getError() != null) {
882      addErrors(vso.getAllErrors());
883      throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError());
884    } else if (vso.getValueset() == null) {
885      throw fail("Unable to expand imported value set "+vs.getUrl()+" but no error");      
886    }
887    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
888      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
889      if (!existsInParams(exp.getParameter(), "used-valueset", u))
890        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
891    }
892    ValueSetExpansionComponent evs = vso.getValueset().getExpansion();
893    for (Extension ex : evs.getExtension()) {
894      if (ex.getUrl().equals(ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
895        if (ex.getValue() instanceof BooleanType) {
896          exp.getExtension().add(new Extension(ToolingExtensions.EXT_EXP_TOOCOSTLY).setValue(new CanonicalType(value)));
897        } else {
898          exp.getExtension().add(ex);
899        }
900      } 
901    }
902    if (!evs.hasTotal()) {
903      // because if there's no total, we can't know if we got everything
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("copyExpansion");
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       }
977       copyExpansion(wc, cc.getContains());
978    }
979  }
980
981  private void addErrors(List<String> errs) {
982    for (String s : errs) {
983      if (!allErrors.contains(s)) {
984        allErrors.add(s);
985      }
986    }
987  }
988
989  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 {
990    int count = 0;
991    opContext.deadCheck("copyImportContains");
992    for (ValueSetExpansionContainsComponent c : list) {
993      c.checkNoModifiers("Imported Expansion in Code System", "expanding");
994      ValueSetExpansionContainsComponent np = addCode(dwc, c.getSystem(), c.getCode(), c.getDisplay(), vsSrc.getLanguage(), parent, null, expParams, c.getAbstract(), c.getInactive(), 
995          filter, noInactive, false, vsProps, makeCSProps(c.getExtensionString(ToolingExtensions.EXT_DEFINITION), null), null, c.getProperty(), null, c.getExtension(), exp);
996      if (np != null) {
997        count++;
998      }
999      count = count + copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps, vsSrc, exp);
1000    }
1001    return count;
1002  }
1003
1004  private void includeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, boolean heirarchical, boolean noInactive, List<Extension> extensions, ValueSet valueSet) throws ETooCostly, FileNotFoundException, IOException, FHIRException, CodeSystemProviderExtension {
1005    opContext.deadCheck("includeCodes");
1006    inc.checkNoModifiers("Compose.include", "expanding");
1007    List<ValueSet> imports = new ArrayList<ValueSet>();
1008    for (CanonicalType imp : inc.getValueSet()) {
1009      imports.add(importValueSet(dwc, imp.getValue(), exp, expParams, noInactive, valueSet));
1010    }
1011
1012    if (!inc.hasSystem()) {
1013      if (imports.isEmpty()) // though this is not supposed to be the case
1014        return;
1015      ValueSet base = imports.get(0);
1016      checkCanonical(exp, base, focus);
1017      imports.remove(0);
1018      base.checkNoModifiers("Imported ValueSet", "expanding");
1019      copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty(), base, exp);
1020    } else {
1021      CodeSystem cs = context.fetchSupplementedCodeSystem(inc.getSystem());
1022      if (ValueSetUtilities.isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) {
1023        doServerIncludeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, valueSet.getExpansion().getProperty());
1024      } else {
1025        if (cs.hasUserData("supplements.installed")) {
1026          for (String s : cs.getUserString("supplements.installed").split("\\,")) {
1027            requiredSupplements.remove(s);
1028          }
1029        }
1030        doInternalIncludeCodes(inc, exp, expParams, imports, cs, noInactive, valueSet);
1031      }
1032    }
1033  }
1034
1035  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 {
1036    opContext.deadCheck("doServerIncludeCodes");
1037    CodeSystemProvider csp = CodeSystemProvider.factory(inc.getSystem());
1038    if (csp != null) {
1039      csp.includeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, vsProps);
1040      return;
1041    }
1042    
1043    ValueSetExpansionOutcome vso = context.expandVS(inc, heirarchical, noInactive);
1044    if (vso.getError() != null) {
1045      throw failTSE("Unable to expand imported value set: " + vso.getError());
1046    }
1047    ValueSet vs = vso.getValueset();
1048    if (vs.hasVersion() || REPORT_VERSION_ANYWAY) {
1049      UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : ""));
1050      if (!existsInParams(exp.getParameter(), "used-valueset", u)) {
1051        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-valueset").setValue(u));
1052      }
1053    }
1054    if (vs.getExpansion().hasTotal()) {
1055      // 0 for now... dwc.incExtraCount(!vs.getExpansion().getTotal());
1056    } else {
1057      dwc.setNoTotal(true);
1058    }
1059    for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) {
1060      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue())) {
1061        exp.getParameter().add(p);
1062      }
1063    }
1064    for (Extension ex : vs.getExpansion().getExtension()) {
1065      if (Utilities.existsInList(ex.getUrl(), ToolingExtensions.EXT_EXP_TOOCOSTLY, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed")) {
1066        if (!ExtensionsUtils.hasExtension(extensions, ex.getUrl())) {
1067          extensions.add(ex);
1068        }
1069      }
1070    }
1071    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
1072      addCodeAndDescendents(dwc, cc, null, expParams, imports, noInactive, vsProps, vs, exp);
1073    }
1074  }
1075
1076
1077  public void doInternalIncludeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive, Resource vsSrc) throws NoTerminologyServiceException, TerminologyServiceException, FHIRException, ETooCostly {
1078    opContext.deadCheck("doInternalIncludeCodes");
1079    if (cs == null) {
1080      if (context.isNoTerminologyServer())
1081        throw failTSE("Unable to find code system " + inc.getSystem().toString());
1082      else
1083        throw failTSE("Unable to find code system " + inc.getSystem().toString());
1084    }
1085    checkCanonical(exp, cs, focus);
1086    cs.checkNoModifiers("Code System", "expanding");
1087    if (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)
1088      throw failTSE("Code system " + inc.getSystem().toString() + " is incomplete");
1089    if (cs.hasVersion() || REPORT_VERSION_ANYWAY) {
1090      UriType u = new UriType(cs.getUrl() + (cs.hasVersion() ? "|"+cs.getVersion() : ""));
1091      if (!existsInParams(exp.getParameter(), "used-codesystem", u))
1092        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-codesystem").setValue(u));
1093      if (cs.hasUserData("supplements.installed")) {
1094        for (String s : cs.getUserString("supplements.installed").split("\\,")) {
1095          u = new UriType(s);
1096          if (!existsInParams(exp.getParameter(), "used-supplement", u)) {
1097            exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("used-supplement").setValue(u));
1098          }
1099        }
1100      }
1101    }
1102    if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
1103      // special case - add all the code system
1104      for (ConceptDefinitionComponent def : cs.getConcept()) {
1105        addCodeAndDescendents(dwc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), null, exp);
1106      }
1107      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1108        addFragmentWarning(exp, cs);
1109      }
1110      if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
1111        addExampleWarning(exp, cs);
1112      }      
1113    }
1114
1115    if (!inc.getConcept().isEmpty()) {
1116      dwc.setCanBeHierarchy(false);
1117      for (ConceptReferenceComponent c : inc.getConcept()) {
1118        c.checkNoModifiers("Code in Value Set", "expanding");
1119        ConceptDefinitionComponent def = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null);
1120        boolean inactive = false; // default is true if we're a fragment and  
1121        boolean isAbstract = false;
1122        if (def == null) {
1123          if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1124            addFragmentWarning(exp, cs);
1125          } else if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
1126              addExampleWarning(exp, cs);
1127          } else {
1128            if (checkCodesWhenExpanding) {
1129              throw failTSE("Unable to find code '" + c.getCode() + "' in code system " + cs.getUrl());
1130            }
1131          }
1132        } else {
1133          def.checkNoModifiers("Code in Code System", "expanding");
1134          inactive = CodeSystemUtilities.isInactive(cs, def);
1135          isAbstract = CodeSystemUtilities.isNotSelectable(cs, def);
1136          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())), 
1137              expParams, isAbstract, inactive, imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), c.getExtension(), exp);
1138        }
1139      }
1140    }
1141    if (inc.getFilter().size() > 0) {
1142      if (inc.getFilter().size() > 1) {
1143        dwc.setCanBeHierarchy(false); // which will be the case if we get around to supporting this
1144      }
1145      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1146        addFragmentWarning(exp, cs);
1147      }
1148      List<WorkingContext> filters = new ArrayList<>();
1149      for (int i = 1; i < inc.getFilter().size(); i++) {
1150        WorkingContext wc = new WorkingContext();
1151        filters.add(wc);
1152        processFilter(inc, exp, expParams, imports, cs, noInactive, inc.getFilter().get(i), wc, null, false);
1153      }
1154      ConceptSetFilterComponent fc = inc.getFilter().get(0);
1155      WorkingContext wc = dwc;
1156      processFilter(inc, exp, expParams, imports, cs, noInactive, fc, wc, filters, false);
1157    }
1158  }
1159
1160  private void processFilter(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive, 
1161      ConceptSetFilterComponent fc, WorkingContext wc, List<WorkingContext> filters, boolean exclude)
1162      throws ETooCostly {
1163    opContext.deadCheck("processFilter");
1164    if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) {
1165      // special: all codes in the target code system under the value
1166      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1167      if (def == null)
1168        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1169      if (exclude) {
1170        excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1171      } else {
1172        addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1173      }
1174    } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISNOTA) {
1175      // special: all codes in the target code system that are not under the value
1176      ConceptDefinitionComponent defEx = getConceptForCode(cs.getConcept(), fc.getValue());
1177      if (defEx == null)
1178        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1179      for (ConceptDefinitionComponent def : cs.getConcept()) {
1180        if (exclude) {
1181          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, defEx, new AllConceptsFilter(allErrors), filters, exp);
1182        } else {
1183          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1184        }
1185      }
1186    } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) {
1187      // special: all codes in the target code system under the value
1188      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1189      if (def == null)
1190        throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
1191      for (ConceptDefinitionComponent c : def.getConcept())
1192        if (exclude) {
1193          excludeCodeAndDescendents(wc, cs, inc.getSystem(), c, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1194        } else {
1195          addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1196        }
1197      if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
1198        List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
1199        for (ConceptDefinitionComponent c : children) {
1200          if (exclude) {
1201            excludeCodeAndDescendents(wc, cs, inc.getSystem(), c, null, imports, null, new AllConceptsFilter(allErrors), filters, exp);
1202          } else {
1203            addCodeAndDescendents(wc, cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(allErrors), noInactive, exp.getProperty(), filters, exp);
1204          }
1205        }
1206      }
1207
1208    } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) {
1209      // gg; note: wtf is this: if the filter is display=v, look up the code 'v', and see if it's display is 'v'?
1210      dwc.setCanBeHierarchy(false);
1211      ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
1212      if (def != null) {
1213        if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) {
1214          if (def.getDisplay().contains(fc.getValue()) && passesOtherFilters(filters, cs, def.getCode())) {
1215            for (String code : getCodesForConcept(def, expParams)) {
1216              opContext.deadCheck("processFilter2");
1217              if (exclude) {
1218                excludeCode(wc, inc.getSystem(), code);
1219              } else {
1220                addCode(wc, inc.getSystem(), code, def.getDisplay(), cs.getLanguage(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def),
1221                  imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), cs, null, def.getExtension(), null, exp);
1222              }
1223            }
1224          }
1225        }
1226      }
1227    } else if (CodeSystemUtilities.isDefinedProperty(cs, fc.getProperty())) {
1228      for (ConceptDefinitionComponent def : cs.getConcept()) {
1229        PropertyFilter pf = new PropertyFilter(allErrors, fc, CodeSystemUtilities.getPropertyDefinition(cs, fc.getProperty()));
1230        if (exclude) {
1231          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, pf, filters, exp);
1232        } else {
1233          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, pf, noInactive, exp.getProperty(), filters, exp);
1234        }
1235      }
1236    } else if (isKnownProperty(fc.getProperty(), cs)) {
1237      for (ConceptDefinitionComponent def : cs.getConcept()) {
1238        KnownPropertyFilter pf = new KnownPropertyFilter(allErrors, fc, fc.getProperty());
1239        if (exclude) {
1240          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, pf, filters, exp);
1241        } else {
1242          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, pf, noInactive, exp.getProperty(), filters, exp);
1243        }
1244      }
1245    } else if ("code".equals(fc.getProperty()) && fc.getOp() == FilterOperator.REGEX) {
1246      for (ConceptDefinitionComponent def : cs.getConcept()) {
1247        if (exclude) {
1248          excludeCodeAndDescendents(wc, cs, inc.getSystem(), def, null, imports, null, new RegexFilter(allErrors, fc.getValue()), filters, exp);
1249        } else {
1250          addCodeAndDescendents(wc, cs, inc.getSystem(), def, null, expParams, imports, null, new RegexFilter(allErrors, fc.getValue()), noInactive, exp.getProperty(), filters, exp);
1251        }
1252      }
1253    } else {
1254      throw fail("Filter by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet");
1255    }
1256  }
1257
1258  private boolean isKnownProperty(String property, CodeSystem cs) {
1259    return Utilities.existsInList(property, "notSelectable");
1260  }
1261
1262  private List<ConceptDefinitionDesignationComponent> mergeDesignations(ConceptDefinitionComponent def,
1263      List<ConceptDefinitionDesignationComponent> list) {
1264    List<ConceptDefinitionDesignationComponent> res = new ArrayList<>();
1265    if (def != null) {
1266      res.addAll(def.getDesignation());
1267    }
1268    res.addAll(list);
1269    return res;
1270  }
1271
1272 
1273
1274  private void addFragmentWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
1275    String url = cs.getVersionedUrl();
1276    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
1277      if ("fragment".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
1278        return;
1279      }     
1280    }
1281    exp.addParameter().setName("fragment").setValue(new CanonicalType(url));
1282  }
1283
1284  private void addExampleWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
1285    String url = cs.getVersionedUrl();
1286    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
1287      if ("example".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
1288        return;
1289      }     
1290    }
1291    exp.addParameter().setName("example").setValue(new CanonicalType(url));
1292  }
1293  
1294  private List<ConceptDefinitionDesignationComponent> convertDesignations(List<ConceptReferenceDesignationComponent> list) {
1295    List<ConceptDefinitionDesignationComponent> res = new ArrayList<CodeSystem.ConceptDefinitionDesignationComponent>();
1296    for (ConceptReferenceDesignationComponent t : list) {
1297      ConceptDefinitionDesignationComponent c = new ConceptDefinitionDesignationComponent();
1298      c.setLanguage(t.getLanguage());
1299      c.setUse(t.getUse());
1300      c.setValue(t.getValue());
1301      c.getExtension().addAll(t.getExtension());
1302      res.add(c);
1303    }
1304    return res;
1305  }
1306
1307  private String key(String uri, String code) {
1308    return "{" + uri + "}" + code;
1309  }
1310
1311  private String key(ValueSetExpansionContainsComponent c) {
1312    return key(c.getSystem(), c.getCode());
1313  }
1314
1315  private FHIRException fail(String msg) {
1316    allErrors.add(msg);
1317    return new FHIRException(msg);
1318  }
1319
1320  private ETooCostly failCostly(String msg) {
1321    allErrors.add(msg);
1322    return new ETooCostly(msg);
1323  }
1324
1325  private TerminologyServiceException failTSE(String msg) {
1326    allErrors.add(msg);
1327    return new TerminologyServiceException(msg);
1328  }
1329
1330  public Collection<? extends String> getAllErrors() {
1331    return allErrors;
1332  }
1333
1334  public boolean isCheckCodesWhenExpanding() {
1335    return checkCodesWhenExpanding;
1336  }
1337
1338  public void setCheckCodesWhenExpanding(boolean checkCodesWhenExpanding) {
1339    this.checkCodesWhenExpanding = checkCodesWhenExpanding;
1340  }
1341
1342  private boolean passesOtherFilters(List<WorkingContext> otherFilters, CodeSystem cs, String code) {
1343    if (otherFilters == null) {
1344      return true;
1345    }
1346    String key = key(cs.getUrl(), code);
1347    for (WorkingContext wc : otherFilters) {
1348      if (!wc.getMap().containsKey(key)) {
1349        return false;
1350      }
1351    }
1352    return true;
1353  }
1354
1355  public boolean isDebug() {
1356    return debug;
1357  }
1358
1359  public ValueSetExpander setDebug(boolean debug) {
1360    this.debug = debug;
1361    return this;
1362  }
1363  
1364  
1365}