001package org.hl7.fhir.r5.terminologies;
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;
038
039/*
040 * Copyright (c) 2011+, HL7, Inc
041 * All rights reserved.
042 * 
043 * Redistribution and use in source and binary forms, with or without modification,
044 * are permitted provided that the following conditions are met:
045 * 
046 * Redistributions of source code must retain the above copyright notice, this
047 * list of conditions and the following disclaimer.
048 * Redistributions in binary form must reproduce the above copyright notice,
049 * this list of conditions and the following disclaimer in the documentation
050 * and/or other materials provided with the distribution.
051 * Neither the name of HL7 nor the names of its contributors may be used to
052 * endorse or promote products derived from this software without specific
053 * prior written permission.
054 * 
055 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
056 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
057 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
058 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
059 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
060 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
061 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
062 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
063 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
064 * POSSIBILITY OF SUCH DAMAGE.
065 * 
066 */
067
068import java.util.ArrayList;
069import java.util.Calendar;
070import java.util.Collection;
071import java.util.GregorianCalendar;
072import java.util.HashMap;
073import java.util.HashSet;
074import java.util.List;
075import java.util.Map;
076import java.util.Set;
077
078import org.apache.commons.lang3.NotImplementedException;
079import org.hl7.fhir.exceptions.FHIRException;
080import org.hl7.fhir.exceptions.FHIRFormatError;
081import org.hl7.fhir.exceptions.NoTerminologyServiceException;
082import org.hl7.fhir.exceptions.TerminologyServiceException;
083import org.hl7.fhir.r5.context.IWorkerContext;
084import org.hl7.fhir.r5.model.BooleanType;
085import org.hl7.fhir.r5.model.CodeSystem;
086import org.hl7.fhir.r5.model.CodeSystem.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.CodeSystem.PropertyType;
092import org.hl7.fhir.r5.model.DataType;
093import org.hl7.fhir.r5.model.DateTimeType;
094import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
095import org.hl7.fhir.r5.model.Extension;
096import org.hl7.fhir.r5.model.Factory;
097import org.hl7.fhir.r5.model.PackageInformation;
098import org.hl7.fhir.r5.model.Parameters;
099import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
100import org.hl7.fhir.r5.model.PrimitiveType;
101import org.hl7.fhir.r5.model.UriType;
102import org.hl7.fhir.r5.model.ValueSet;
103import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
104import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
105import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
106import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
107import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent;
108import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent;
109import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
110import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent;
111import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent;
112import org.hl7.fhir.r5.utils.ToolingExtensions;
113import org.hl7.fhir.utilities.Utilities;
114
115import com.google.errorprone.annotations.NoAllocation;
116
117public class ValueSetExpanderSimple extends ValueSetWorker implements ValueSetExpander {
118
119  public class PropertyFilter implements IConceptFilter {
120
121    private ConceptSetFilterComponent filter;
122    private PropertyComponent property;
123
124    public PropertyFilter(ConceptSetFilterComponent fc, PropertyComponent propertyDefinition) {
125      this.filter = fc;
126      this.property = propertyDefinition;
127    }
128
129    @Override
130    public boolean includeConcept(CodeSystem cs, ConceptDefinitionComponent def) {
131      ConceptPropertyComponent pc = getPropertyForConcept(def);
132      if (pc != null) {
133        String v = pc.getValue().isPrimitive() ? pc.getValue().primitiveValue() : null;
134        switch (filter.getOp()) {
135        case DESCENDENTOF: throw fail("not supported yet: "+filter.getOp().toCode());
136        case EQUAL: return filter.getValue().equals(v);
137        case EXISTS: throw fail("not supported yet: "+filter.getOp().toCode());
138        case GENERALIZES: throw fail("not supported yet: "+filter.getOp().toCode());
139        case IN: throw fail("not supported yet: "+filter.getOp().toCode());
140        case ISA: throw fail("not supported yet: "+filter.getOp().toCode());
141        case ISNOTA: throw fail("not supported yet: "+filter.getOp().toCode());
142        case NOTIN: throw fail("not supported yet: "+filter.getOp().toCode());
143        case NULL: throw fail("not supported yet: "+filter.getOp().toCode());
144        case REGEX: throw fail("not supported yet: "+filter.getOp().toCode());
145        default:
146          throw fail("Shouldn't get here");        
147        }            
148      } else if (property.getType() == PropertyType.BOOLEAN && filter.getOp() == FilterOperator.EQUAL) {
149        return "false".equals(filter.getValue()); 
150      } else {
151        return false;
152      }
153    }
154
155    private ConceptPropertyComponent getPropertyForConcept(ConceptDefinitionComponent def) {
156      for (ConceptPropertyComponent pc : def.getProperty()) {
157        if (pc.getCode().equals(property.getCode())) {
158          return pc;
159        }
160      }
161      return null;
162    }
163
164  }
165
166  public class AllConceptsFilter implements IConceptFilter {
167
168    @Override
169    public boolean includeConcept(CodeSystem cs, ConceptDefinitionComponent def) {
170      return true;
171    }
172  }
173
174  public interface IConceptFilter {
175
176    boolean includeConcept(CodeSystem cs, ConceptDefinitionComponent def);
177
178  }
179
180  private List<ValueSetExpansionContainsComponent> codes = new ArrayList<ValueSet.ValueSetExpansionContainsComponent>();
181  private List<ValueSetExpansionContainsComponent> roots = new ArrayList<ValueSet.ValueSetExpansionContainsComponent>();
182  private Map<String, ValueSetExpansionContainsComponent> map = new HashMap<String, ValueSet.ValueSetExpansionContainsComponent>();
183  private IWorkerContext context;
184  private boolean canBeHeirarchy = true;
185  private boolean includeAbstract = true;
186  private Set<String> excludeKeys = new HashSet<String>();
187  private Set<String> excludeSystems = new HashSet<String>();
188  private ValueSet focus;
189  private int maxExpansionSize = 500;
190  private List<String> allErrors = new ArrayList<>();
191
192  private int total;
193  private boolean checkCodesWhenExpanding;
194
195  public ValueSetExpanderSimple(IWorkerContext context) {
196    super();
197    this.context = context;
198  }
199
200  public ValueSetExpanderSimple(IWorkerContext context, List<String> allErrors) {
201    super();
202    this.context = context;
203    this.allErrors = allErrors;
204  }
205
206  public void setMaxExpansionSize(int theMaxExpansionSize) {
207    maxExpansionSize = theMaxExpansionSize;
208  }
209  
210  private ValueSetExpansionContainsComponent addCode(String system, String code, String display, ValueSetExpansionContainsComponent parent, List<ConceptDefinitionDesignationComponent> designations, Parameters expParams, 
211      boolean isAbstract, boolean inactive, List<ValueSet> filters, boolean noInactive, boolean deprecated, List<ValueSetExpansionPropertyComponent> vsProp) {
212 
213    if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code))
214      return null;
215    if (noInactive && inactive) {
216      return null;
217    }
218    
219    ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
220    n.setSystem(system);
221    n.setCode(code);
222    if (isAbstract)
223      n.setAbstract(true);
224    if (inactive)
225      n.setInactive(true);
226    if (deprecated) {
227      ValueSetUtilities.setDeprecated(vsProp, n);
228    }
229
230    if (expParams.getParameterBool("includeDesignations") && designations != null) {
231      for (ConceptDefinitionDesignationComponent t : designations) {
232        if (t.getLanguage() != null || t.getValue() != null) {
233          ConceptReferenceDesignationComponent d = n.addDesignation();
234          if (t.getLanguage() != null) {
235            d.setLanguage(t.getLanguage().trim());
236          }
237          if (t.getValue() != null) {
238            d.setValue(t.getValue().trim());
239          }
240        }
241      }
242    }
243    ConceptDefinitionDesignationComponent t = expParams.hasLanguage() ? getMatchingLang(designations, expParams.getLanguage()) : null;
244    if (t == null)
245      n.setDisplay(display);
246    else
247      n.setDisplay(t.getValue());
248
249    String s = key(n);
250    if (map.containsKey(s) || excludeKeys.contains(s)) {
251      canBeHeirarchy = false;
252    } else {
253      codes.add(n);
254      map.put(s, n);
255      total++;
256    }
257    if (canBeHeirarchy && parent != null) {
258      parent.getContains().add(n);
259    } else {
260      roots.add(n);
261    }
262    return n;
263  }
264
265  private boolean filterContainsCode(List<ValueSet> filters, String system, String code) {
266    for (ValueSet vse : filters)
267      if (expansionContainsCode(vse.getExpansion().getContains(), system, code))
268        return true;
269    return false;
270  }
271
272  private boolean expansionContainsCode(List<ValueSetExpansionContainsComponent> contains, String system, String code) {
273    for (ValueSetExpansionContainsComponent cc : contains) {
274      if (system.equals(cc.getSystem()) && code.equals(cc.getCode()))
275        return true;
276      if (expansionContainsCode(cc.getContains(), system, code))
277        return true;
278    }
279    return false;
280  }
281
282  private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, String lang) {
283    for (ConceptDefinitionDesignationComponent t : list)
284      if (t.getLanguage().equals(lang))
285        return t;
286    for (ConceptDefinitionDesignationComponent t : list)
287      if (t.getLanguage().startsWith(lang))
288        return t;
289    return null;
290  }
291
292  private void addCodeAndDescendents(ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps)  throws FHIRException {
293    focus.checkNoModifiers("Expansion.contains", "expanding");
294    ValueSetExpansionContainsComponent np = addCode(focus.getSystem(), focus.getCode(), focus.getDisplay(), parent, 
295         convert(focus.getDesignation()), expParams, focus.getAbstract(), focus.getInactive(), filters, noInactive, false, vsProps);
296    for (ValueSetExpansionContainsComponent c : focus.getContains())
297      addCodeAndDescendents(focus, np, expParams, filters, noInactive, vsProps);
298  }
299  
300  private List<ConceptDefinitionDesignationComponent> convert(List<ConceptReferenceDesignationComponent> designations) {
301    List<ConceptDefinitionDesignationComponent> list = new ArrayList<ConceptDefinitionDesignationComponent>();
302    for (ConceptReferenceDesignationComponent d : designations) {
303      ConceptDefinitionDesignationComponent n = new ConceptDefinitionDesignationComponent();
304      n.setLanguage(d.getLanguage());
305      n.setUse(d.getUse());
306      n.setValue(d.getValue());
307      list.add(n);
308    }
309    return list;
310  }
311
312  private void addCodeAndDescendents(CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, ConceptDefinitionComponent exclusion, IConceptFilter filterFunc, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps)  throws FHIRException {
313    def.checkNoModifiers("Code in Code System", "expanding");
314    if (exclusion != null) {
315      if (exclusion.getCode().equals(def.getCode()))
316        return; // excluded.
317    }
318    ValueSetExpansionContainsComponent np = null;
319    boolean abs = CodeSystemUtilities.isNotSelectable(cs, def);
320    boolean inc = CodeSystemUtilities.isInactive(cs, def);
321    boolean dep = CodeSystemUtilities.isDeprecated(cs, def, false);
322    if ((includeAbstract || !abs)  && filterFunc.includeConcept(cs, def)) {
323      np = addCode(system, def.getCode(), def.getDisplay(), parent, def.getDesignation(), expParams, abs, inc, filters, noInactive, dep, vsProps);
324    }
325    for (ConceptDefinitionComponent c : def.getConcept()) {
326      addCodeAndDescendents(cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps);
327    }
328    if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
329      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
330      for (ConceptDefinitionComponent c : children)
331        addCodeAndDescendents(cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps);
332    }
333
334  }
335
336  private void addCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws ETooCostly, FHIRException {
337    if (expand != null) {
338      if (expand.getContains().size() > maxExpansionSize)
339        throw failCostly("Too many codes to display (>" + Integer.toString(expand.getContains().size()) + ")");
340      for (ValueSetExpansionParameterComponent p : expand.getParameter()) {
341        if (!existsInParams(params, p.getName(), p.getValue()))
342          params.add(p);
343      }
344
345      copyImportContains(expand.getContains(), null, expParams, filters, noInactive, vsProps);
346    }
347  }
348
349  private void excludeCode(String theSystem, String theCode) {
350    ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
351    n.setSystem(theSystem);
352    n.setCode(theCode);
353    String s = key(n);
354    excludeKeys.add(s);
355  }
356
357  private void excludeCodes(ConceptSetComponent exc, List<ValueSetExpansionParameterComponent> params, String ctxt) throws FHIRException {
358    exc.checkNoModifiers("Compose.exclude", "expanding");
359    if (exc.hasSystem() && exc.getConcept().size() == 0 && exc.getFilter().size() == 0) {
360      excludeSystems.add(exc.getSystem());
361    }
362
363    if (exc.hasValueSet())
364      throw fail("Processing Value set references in exclude is not yet done in "+ctxt);
365    // importValueSet(imp.getValue(), params, expParams);
366
367    CodeSystem cs = context.fetchCodeSystem(exc.getSystem());
368    if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(exc.getSystem())) {
369      ValueSetExpansionOutcome vse = context.expandVS(exc, false, false);
370      ValueSet valueset = vse.getValueset();
371      if (valueset == null)
372        throw failTSE("Error Expanding ValueSet: "+vse.getError());
373      excludeCodes(valueset.getExpansion(), params);
374      return;
375    }
376
377    for (ConceptReferenceComponent c : exc.getConcept()) {
378      excludeCode(exc.getSystem(), c.getCode());
379    }
380
381    if (exc.getFilter().size() > 0)
382      throw fail("not done yet - multiple filters");
383  }
384
385
386
387  private void excludeCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params) {
388    for (ValueSetExpansionContainsComponent c : expand.getContains()) {
389      excludeCode(c.getSystem(), c.getCode());
390    }
391  }
392
393  private boolean existsInParams(List<ValueSetExpansionParameterComponent> params, String name, DataType value) {
394    for (ValueSetExpansionParameterComponent p : params) {
395      if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false))
396        return true;
397    }
398    return false;
399  }
400
401  @Override
402  public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) {
403    allErrors.clear();
404    try {
405      return expandInternal(source, expParams);
406    } catch (NoTerminologyServiceException e) {
407      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
408      // that might fail too, but it might not, later.
409      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.NOSERVICE, allErrors);
410    } catch (Exception e) {
411      // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set
412      // that might fail too, but it might not, later.
413      return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.UNKNOWN, allErrors);
414    }
415  }
416  
417  public ValueSetExpansionOutcome expandInternal(ValueSet source, Parameters expParams) throws FHIRException, FileNotFoundException, ETooCostly, IOException {
418      return doExpand(source, expParams);
419  }
420
421  public ValueSetExpansionOutcome doExpand(ValueSet source, Parameters expParams) throws FHIRException, ETooCostly, FileNotFoundException, IOException {
422    if (expParams == null)
423      expParams = makeDefaultExpansion();
424    source.checkNoModifiers("ValueSet", "expanding");
425    focus = source.copy();
426    focus.setExpansion(new ValueSet.ValueSetExpansionComponent());
427    focus.getExpansion().setTimestampElement(DateTimeType.now());
428    focus.getExpansion().setIdentifier(Factory.createUUID()); 
429    for (ParametersParameterComponent p : expParams.getParameter()) {
430      if (Utilities.existsInList(p.getName(), "includeDesignations", "excludeNested", "activeOnly"))
431        focus.getExpansion().addParameter().setName(p.getName()).setValue(p.getValue());
432    }
433
434    if (source.hasCompose()) {
435      handleCompose(source.getCompose(), focus.getExpansion(), expParams, source.getUrl(), focus.getExpansion().getExtension(), source);
436    }
437
438    if (canBeHeirarchy) {
439      for (ValueSetExpansionContainsComponent c : roots) {
440        focus.getExpansion().getContains().add(c);
441      }
442    } else {
443      for (ValueSetExpansionContainsComponent c : codes) {
444        if (map.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
445          focus.getExpansion().getContains().add(c);
446          c.getContains().clear(); // make sure any heirarchy is wiped
447        }
448      }
449    }
450
451    if (total > 0) {
452      focus.getExpansion().setTotal(total);
453    }
454
455    return new ValueSetExpansionOutcome(focus);
456  }
457
458  private Parameters makeDefaultExpansion() {
459    Parameters res = new Parameters();
460    res.addParameter("excludeNested", true);
461    res.addParameter("includeDesignations", false);
462    return res;
463  }
464
465  private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> clist, String code) {
466    for (ConceptDefinitionComponent c : clist) {
467      if (code.equals(c.getCode()))
468        return c;
469      ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code);
470      if (v != null)
471        return v;
472    }
473    return null;
474  }
475
476  private void handleCompose(ValueSetComposeComponent compose, ValueSetExpansionComponent exp, Parameters expParams, String ctxt, List<Extension> extensions, ValueSet valueSet)
477      throws ETooCostly, FileNotFoundException, IOException, FHIRException {
478    compose.checkNoModifiers("ValueSet.compose", "expanding");
479    // Exclude comes first because we build up a map of things to exclude
480    for (ConceptSetComponent inc : compose.getExclude())
481      excludeCodes(inc, exp.getParameter(), ctxt);
482    canBeHeirarchy = !expParams.getParameterBool("excludeNested") && excludeKeys.isEmpty() && excludeSystems.isEmpty();
483    includeAbstract = !expParams.getParameterBool("excludeNotForUI");
484    boolean first = true;
485    for (ConceptSetComponent inc : compose.getInclude()) {
486      if (first == true)
487        first = false;
488      else
489        canBeHeirarchy = false;
490      includeCodes(inc, exp, expParams, canBeHeirarchy, compose.hasInactive() ? !compose.getInactive() : checkNoInActiveFromParam(expParams), extensions, valueSet);
491    }
492  }
493
494  /**
495   * returns true if activeOnly = true 
496   * @param expParams
497   * @return
498   */
499  private boolean checkNoInActiveFromParam(Parameters expParams) {
500    for (ParametersParameterComponent p : expParams.getParameter()) {
501      if (p.getName().equals("activeOnly")) {
502        return p.getValueBooleanType().getValue();
503      }
504    }
505    return false;
506  }
507
508  private ValueSet importValueSet(String value, ValueSetExpansionComponent exp, Parameters expParams, boolean noInactive, ValueSet valueSet) throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError {
509    if (value == null)
510      throw fail("unable to find value set with no identity");
511    ValueSet vs = context.fetchResource(ValueSet.class, value, valueSet);
512    if (vs == null) {
513      if (context.fetchResource(CodeSystem.class, value, valueSet) != null) {
514        throw fail("Cannot include value set "+value+" because it's actually a code system");
515      } else {
516        throw fail("Unable to find imported value set " + value);
517      }
518    }
519    if (noInactive) {
520      expParams = expParams.copy();
521      expParams.addParameter("activeOnly", true);
522    }
523    ValueSetExpansionOutcome vso = new ValueSetExpanderSimple(context, allErrors).expand(vs, expParams);
524    if (vso.getError() != null) {
525      addErrors(vso.getAllErrors());
526      throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError());
527    }
528    if (vs.hasVersion())
529      if (!existsInParams(exp.getParameter(), "version", new UriType(vs.getUrl() + "|" + vs.getVersion())))
530        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(vs.getUrl() + "|" + vs.getVersion())));
531    for (Extension ex : vso.getValueset().getExpansion().getExtension()) {
532      if (ex.getUrl().equals(ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
533        if (ex.getValue() instanceof BooleanType) {
534          exp.getExtension().add(new Extension(ToolingExtensions.EXT_EXP_TOOCOSTLY).setValue(new UriType(value)));
535        } else {
536          exp.getExtension().add(ex);
537        }
538      } 
539    }
540    for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) {
541      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue()))
542        exp.getParameter().add(p);
543    }
544    if (isValueSetUnionImports(valueSet)) {
545      copyExpansion(vso.getValueset().getExpansion().getContains());
546    }
547    canBeHeirarchy = false; // if we're importing a value set, we have to be combining, so we won't try for a heirarchy
548    return vso.getValueset();
549  }
550  
551
552  protected boolean isValueSetUnionImports(ValueSet valueSet) {
553    PackageInformation p = valueSet.getSourcePackage();
554    if (p != null) {
555      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
556    } else {
557      return false;
558    }
559  }
560
561  public void copyExpansion(List<ValueSetExpansionContainsComponent> list) {
562    for (ValueSetExpansionContainsComponent cc : list) {
563       ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent();
564       n.setSystem(cc.getSystem());
565       n.setCode(cc.getCode());
566       n.setAbstract(cc.getAbstract());
567       n.setInactive(cc.getInactive());
568       n.setDisplay(cc.getDisplay());
569       n.getDesignation().addAll(cc.getDesignation());
570
571       String s = key(n);
572       if (!map.containsKey(s) && !excludeKeys.contains(s)) {
573         codes.add(n);
574         map.put(s, n);
575         total++;
576       }
577       copyExpansion(cc.getContains());
578    }
579  }
580
581  private void addErrors(List<String> errs) {
582    for (String s : errs) {
583      if (!allErrors.contains(s)) {
584        allErrors.add(s);
585      }
586    }
587  }
588
589  private void copyImportContains(List<ValueSetExpansionContainsComponent> list, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filter, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException {
590    for (ValueSetExpansionContainsComponent c : list) {
591      c.checkNoModifiers("Imported Expansion in Code System", "expanding");
592      ValueSetExpansionContainsComponent np = addCode(c.getSystem(), c.getCode(), c.getDisplay(), parent, null, expParams, c.getAbstract(), c.getInactive(), filter, noInactive, false, vsProps);
593      copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps);
594    }
595  }
596
597  private void includeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, boolean heirarchical, boolean noInactive, List<Extension> extensions, ValueSet valueSet) throws ETooCostly, FileNotFoundException, IOException, FHIRException {
598    inc.checkNoModifiers("Compose.include", "expanding");
599    List<ValueSet> imports = new ArrayList<ValueSet>();
600    for (UriType imp : inc.getValueSet()) {
601      imports.add(importValueSet(imp.getValue(), exp, expParams, noInactive, valueSet));
602    }
603
604    if (!inc.hasSystem()) {
605      if (imports.isEmpty()) // though this is not supposed to be the case
606        return;
607      ValueSet base = imports.get(0);
608      imports.remove(0);
609      base.checkNoModifiers("Imported ValueSet", "expanding");
610      copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty());
611    } else {
612      CodeSystem cs = context.fetchCodeSystem(inc.getSystem());
613      if (isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) {
614        doServerIncludeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, valueSet.getExpansion().getProperty());
615      } else {
616        doInternalIncludeCodes(inc, exp, expParams, imports, cs, noInactive);
617      }
618    }
619  }
620
621  private void doServerIncludeCodes(ConceptSetComponent inc, boolean heirarchical, ValueSetExpansionComponent exp, List<ValueSet> imports, Parameters expParams, List<Extension> extensions, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException {
622    ValueSetExpansionOutcome vso = context.expandVS(inc, heirarchical, noInactive);
623    if (vso.getError() != null) {
624      throw failTSE("Unable to expand imported value set: " + vso.getError());
625    }
626    ValueSet vs = vso.getValueset();
627    if (vs.hasVersion()) {
628      if (!existsInParams(exp.getParameter(), "version", new UriType(vs.getUrl() + "|" + vs.getVersion()))) {
629        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(vs.getUrl() + "|" + vs.getVersion())));
630      }
631    }
632    for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) {
633      if (!existsInParams(exp.getParameter(), p.getName(), p.getValue())) {
634        exp.getParameter().add(p);
635      }
636    }
637    for (Extension ex : vs.getExpansion().getExtension()) {
638      if (Utilities.existsInList(ex.getUrl(), ToolingExtensions.EXT_EXP_TOOCOSTLY, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed")) {
639        if (!hasExtension(extensions, ex.getUrl())) {
640          extensions.add(ex);
641        }
642      }
643    }
644    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
645      addCodeAndDescendents(cc, null, expParams, imports, noInactive, vsProps);
646    }
647  }
648
649  private boolean hasExtension(List<Extension> extensions, String url) {
650    for (Extension ex : extensions) {
651      if (ex.getUrl().equals(url)) {
652        return true;
653      }
654    }
655    return false;
656  }
657
658  public void doInternalIncludeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive) throws NoTerminologyServiceException, TerminologyServiceException, FHIRException {
659    if (cs == null) {
660      if (context.isNoTerminologyServer())
661        throw failTSE("Unable to find code system " + inc.getSystem().toString());
662      else
663        throw failTSE("Unable to find code system " + inc.getSystem().toString());
664    }
665    cs.checkNoModifiers("Code System", "expanding");
666    if (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)
667      throw failTSE("Code system " + inc.getSystem().toString() + " is incomplete");
668    if (cs.hasVersion())
669      if (!existsInParams(exp.getParameter(), "version", new UriType(cs.getUrl() + "|" + cs.getVersion())))
670        exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(cs.getUrl() + "|" + cs.getVersion())));
671
672    if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
673      // special case - add all the code system
674      for (ConceptDefinitionComponent def : cs.getConcept()) {
675        addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty());
676      }
677      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
678        addFragmentWarning(exp, cs);
679      }
680      if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
681        addExampleWarning(exp, cs);
682      }      
683    }
684
685    if (!inc.getConcept().isEmpty()) {
686      canBeHeirarchy = false;
687      for (ConceptReferenceComponent c : inc.getConcept()) {
688        c.checkNoModifiers("Code in Code System", "expanding");
689        ConceptDefinitionComponent def = CodeSystemUtilities.findCode(cs.getConcept(), c.getCode());
690        Boolean inactive = false; // default is true if we're a fragment and  
691        if (def == null) {
692          if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
693            addFragmentWarning(exp, cs);
694          } else if (cs.getContent() == CodeSystemContentMode.EXAMPLE) {
695              addExampleWarning(exp, cs);
696          } else {
697            if (checkCodesWhenExpanding) {
698              throw failTSE("Unable to find code '" + c.getCode() + "' in code system " + cs.getUrl());
699            }
700          }
701        } else {
702          inactive = CodeSystemUtilities.isInactive(cs, def);
703        }
704        addCode(inc.getSystem(), c.getCode(), !Utilities.noString(c.getDisplay()) ? c.getDisplay() : def == null ? null : def.getDisplay(), null, convertDesignations(c.getDesignation()), expParams, false, inactive, imports, noInactive, false, exp.getProperty());
705      }
706    }
707    if (inc.getFilter().size() > 1) {
708      canBeHeirarchy = false; // which will bt the case if we get around to supporting this
709      throw failTSE("Multiple filters not handled yet"); // need to and them, and this isn't done yet. But this shouldn't arise in non loinc and snomed value sets
710    }
711    if (inc.getFilter().size() == 1) {
712      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
713        addFragmentWarning(exp, cs);
714      }
715      ConceptSetFilterComponent fc = inc.getFilter().get(0);
716      if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) {
717        // special: all codes in the target code system under the value
718        ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
719        if (def == null)
720          throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
721        addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty());
722      } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISNOTA) {
723        // special: all codes in the target code system that are not under the value
724        ConceptDefinitionComponent defEx = getConceptForCode(cs.getConcept(), fc.getValue());
725        if (defEx == null)
726          throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
727        for (ConceptDefinitionComponent def : cs.getConcept()) {
728          addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(), noInactive, exp.getProperty());
729        }
730      } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) {
731        // special: all codes in the target code system under the value
732        ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
733        if (def == null)
734          throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'");
735        for (ConceptDefinitionComponent c : def.getConcept())
736          addCodeAndDescendents(cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty());
737        if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
738          List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
739          for (ConceptDefinitionComponent c : children)
740            addCodeAndDescendents(cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty());
741        }
742
743      } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) {
744        // gg; note: wtf is this: if the filter is display=v, look up the code 'v', and see if it's diplsay is 'v'?
745        canBeHeirarchy = false;
746        ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue());
747        if (def != null) {
748          if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) {
749            if (def.getDisplay().contains(fc.getValue())) {
750              addCode(inc.getSystem(), def.getCode(), def.getDisplay(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def),
751                  imports, noInactive, false, exp.getProperty());
752            }
753          }
754        }
755      } else if (isDefinedProperty(cs, fc.getProperty())) {
756        for (ConceptDefinitionComponent def : cs.getConcept()) {
757          addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, null, new PropertyFilter(fc, getPropertyDefinition(cs, fc.getProperty())), noInactive, exp.getProperty());
758        }
759      } else {
760        throw fail("Search by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet");
761      }
762    }
763  }
764
765  private PropertyComponent getPropertyDefinition(CodeSystem cs, String property) {
766    for (PropertyComponent cp : cs.getProperty()) {
767      if (cp.getCode().equals(property)) {
768        return cp;
769      }
770    }
771    return null;
772  }
773
774  private boolean isDefinedProperty(CodeSystem cs, String property) {
775    for (PropertyComponent cp : cs.getProperty()) {
776      if (cp.getCode().equals(property)) {
777        return true;
778      }
779    }
780    return false;
781  }
782
783  private void addFragmentWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
784    String url = cs.getVersionedUrl();
785    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
786      if ("fragment".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
787        return;
788      }     
789    }
790    exp.addParameter().setName("fragment").setValue(new UriType(url));
791  }
792
793  private void addExampleWarning(ValueSetExpansionComponent exp, CodeSystem cs) {
794    String url = cs.getVersionedUrl();
795    for (ValueSetExpansionParameterComponent p : exp.getParameter()) {
796      if ("example".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 
797        return;
798      }     
799    }
800    exp.addParameter().setName("example").setValue(new UriType(url));
801  }
802  
803  private List<ConceptDefinitionDesignationComponent> convertDesignations(List<ConceptReferenceDesignationComponent> list) {
804    List<ConceptDefinitionDesignationComponent> res = new ArrayList<CodeSystem.ConceptDefinitionDesignationComponent>();
805    for (ConceptReferenceDesignationComponent t : list) {
806      ConceptDefinitionDesignationComponent c = new ConceptDefinitionDesignationComponent();
807      c.setLanguage(t.getLanguage());
808      c.setUse(t.getUse());
809      c.setValue(t.getValue());
810    }
811    return res;
812  }
813
814  private String key(String uri, String code) {
815    return "{" + uri + "}" + code;
816  }
817
818  private String key(ValueSetExpansionContainsComponent c) {
819    return key(c.getSystem(), c.getCode());
820  }
821
822  private FHIRException fail(String msg) {
823    allErrors.add(msg);
824    return new FHIRException(msg);
825  }
826
827  private ETooCostly failCostly(String msg) {
828    allErrors.add(msg);
829    return new ETooCostly(msg);
830  }
831
832  private TerminologyServiceException failTSE(String msg) {
833    allErrors.add(msg);
834    return new TerminologyServiceException(msg);
835  }
836
837  public Collection<? extends String> getAllErrors() {
838    return allErrors;
839  }
840
841  public boolean isCheckCodesWhenExpanding() {
842    return checkCodesWhenExpanding;
843  }
844
845  public void setCheckCodesWhenExpanding(boolean checkCodesWhenExpanding) {
846    this.checkCodesWhenExpanding = checkCodesWhenExpanding;
847  }
848
849}