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