001package org.hl7.fhir.r4b.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 java.util.ArrayList;
035import java.util.HashMap;
036import java.util.List;
037import java.util.Map;
038
039import org.hl7.fhir.exceptions.FHIRException;
040import org.hl7.fhir.exceptions.NoTerminologyServiceException;
041import org.hl7.fhir.r4b.context.IWorkerContext;
042import org.hl7.fhir.r4b.context.IWorkerContext.ValidationResult;
043import org.hl7.fhir.r4b.model.CanonicalType;
044import org.hl7.fhir.r4b.model.CodeSystem;
045import org.hl7.fhir.r4b.model.CodeSystem.CodeSystemContentMode;
046import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionComponent;
047import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionDesignationComponent;
048import org.hl7.fhir.r4b.model.CodeableConcept;
049import org.hl7.fhir.r4b.model.Coding;
050import org.hl7.fhir.r4b.model.Enumerations.PublicationStatus;
051import org.hl7.fhir.r4b.model.UriType;
052import org.hl7.fhir.r4b.model.ValueSet;
053import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceComponent;
054import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceDesignationComponent;
055import org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent;
056import org.hl7.fhir.r4b.model.ValueSet.ConceptSetFilterComponent;
057import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent;
058import org.hl7.fhir.r4b.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
059import org.hl7.fhir.r4b.utils.ToolingExtensions;
060import org.hl7.fhir.r4b.utils.validation.ValidationContextCarrier;
061import org.hl7.fhir.r4b.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
062import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
063import org.hl7.fhir.utilities.Utilities;
064import org.hl7.fhir.utilities.i18n.I18nConstants;
065import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
066import org.hl7.fhir.utilities.validation.ValidationOptions;
067import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode;
068
069public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChecker {
070
071  private ValueSet valueset;
072  private IWorkerContext context;
073  private Map<String, ValueSetCheckerSimple> inner = new HashMap<>();
074  private ValidationOptions options;
075  private ValidationContextCarrier localContext;
076  private List<CodeSystem> localSystems = new ArrayList<>();
077
078  public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) {
079    this.valueset = source;
080    this.context = context;
081    this.options = options;
082  }
083  
084  public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt) {
085    this.valueset = source;
086    this.context = context;
087    this.options = options;
088    this.localContext = ctxt;
089    analyseValueSet();
090  }
091
092  private void analyseValueSet() {
093    if (localContext != null) {
094      if (valueset != null) {
095        for (ConceptSetComponent i : valueset.getCompose().getInclude()) {
096          analyseComponent(i);
097        }
098        for (ConceptSetComponent i : valueset.getCompose().getExclude()) {
099          analyseComponent(i);
100        }
101      }
102    }
103  }
104
105  private void analyseComponent(ConceptSetComponent i) {
106    if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) {
107      String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM);
108      if (ref.startsWith("#")) {
109        String id = ref.substring(1);
110        for (ValidationContextResourceProxy t : localContext.getResources()) {
111          CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class);
112          if (cs != null) {
113            localSystems.add(cs);
114          }
115        }
116      } else {        
117        throw new Error("Not done yet #2: "+ref);
118      }
119    }    
120  }
121
122  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
123    // first, we validate the codings themselves
124    List<String> errors = new ArrayList<String>();
125    List<String> warnings = new ArrayList<String>();
126    if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) {
127      for (Coding c : code.getCoding()) {
128        if (!c.hasSystem()) {
129          warnings.add(context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE));
130        }
131        CodeSystem cs = resolveCodeSystem(c.getSystem());
132        ValidationResult res = null;
133        if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) {
134          res = context.validateCode(options.noClient(), c, null);
135        } else {
136          res = validateCode(c, cs);
137        }
138        if (!res.isOk()) {
139          errors.add(res.getMessage());
140        } else if (res.getMessage() != null) {
141          warnings.add(res.getMessage());
142        }
143      }
144    }
145    if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
146      Boolean result = false;
147      for (Coding c : code.getCoding()) {
148        Boolean ok = codeInValueSet(c.getSystem(), c.getCode(), warnings);
149        if (ok == null && result == false) {
150          result = null;
151        } else if (ok) {
152          result = true;
153        }
154      }
155      if (result == null) {
156        warnings.add(0, context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl()));        
157      } else if (!result) {
158        errors.add(0, context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl()));
159      }
160    }
161    if (errors.size() > 0) {
162      return new ValidationResult(IssueSeverity.ERROR, errors.toString());
163    } else if (warnings.size() > 0) {
164      return new ValidationResult(IssueSeverity.WARNING, warnings.toString());
165    } else { 
166      return new ValidationResult(IssueSeverity.INFORMATION, null);
167    }
168  }
169
170  public CodeSystem resolveCodeSystem(String system) {
171    for (CodeSystem t : localSystems) {
172      if (t.getUrl().equals(system)) {
173        return t;
174      }
175    }
176    CodeSystem cs = context.fetchCodeSystem(system);
177    if (cs == null) {
178      cs = findSpecialCodeSystem(system);
179    }
180    return cs;
181  }
182
183  public ValidationResult validateCode(Coding code) throws FHIRException {
184    String warningMessage = null;
185    // first, we validate the concept itself
186
187    ValidationResult res = null;
188    boolean inExpansion = false;
189    boolean inInclude = false;
190    String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull();
191    if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) {
192      if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum)
193        system = systemForCodeInValueSet(code.getCode());
194      }
195      if (!code.hasSystem()) {
196        if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) {
197          system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets
198        }
199        code.setSystem(system);
200      }
201      inExpansion = checkExpansion(code);
202      inInclude = checkInclude(code);
203      CodeSystem cs = resolveCodeSystem(system);
204      if (cs == null) {
205        warningMessage = "Unable to resolve system "+system;
206        if (!inExpansion) {
207          if (valueset != null && valueset.hasExpansion()) {
208            return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, valueset.getUrl(), code.getCode().toString(), code.getSystem()));
209          } else {
210            throw new FHIRException(warningMessage);
211          }
212        }
213      }
214      if (cs != null && cs.hasSupplements()) {
215        return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl()));        
216      }
217      if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
218        warningMessage = "Resolved system "+system+", but the definition is not complete";
219        if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment
220          throw new FHIRException(warningMessage);
221        }
222      }
223
224      if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) {
225        if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) {
226          // we can't validate that here. 
227          throw new FHIRException("Unable to evaluate based on empty code system");
228        }
229        res = validateCode(code, cs);
230      } else if (cs == null && valueset.hasExpansion() && inExpansion) {
231        // we just take the value set as face value then
232        res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()));
233      } else {
234        // well, we didn't find a code system - try the expansion? 
235        // disabled waiting for discussion
236        throw new FHIRException("No try the server");
237      }
238    } else {
239      inExpansion = checkExpansion(code);
240      inInclude = checkInclude(code);
241    }
242
243    List<String> warnings = new ArrayList<>();
244    
245    // then, if we have a value set, we check it's in the value set
246    if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
247      if ((res==null || res.isOk())) { 
248        Boolean ok = codeInValueSet(system, code.getCode(), warnings);
249        if (ok == null || !ok) {
250          if (res == null) {
251            res = new ValidationResult((IssueSeverity) null, null);
252          }
253          if (!inExpansion && !inInclude) {
254            res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR);
255          } else if (warningMessage!=null) {
256            res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage));
257          } else if (inExpansion) {
258            res.setMessage("Code found in expansion, however: " + res.getMessage());
259          } else if (inInclude) {
260            res.setMessage("Code found in include, however: " + res.getMessage());
261          }
262        }
263      }
264    }
265    return res;
266  }
267
268  private boolean checkInclude(Coding code) {
269    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
270      return false;
271    }
272    for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
273      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
274        for (ConceptReferenceComponent cc : inc.getConcept()) {
275          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
276            return false;
277          }
278        }
279      }
280    }
281    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
282      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
283        for (ConceptReferenceComponent cc : inc.getConcept()) {
284          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
285            return true;
286          }
287        }
288      }
289    }
290    return false;
291  }
292
293  private CodeSystem findSpecialCodeSystem(String system) {
294    if ("urn:ietf:rfc:3986".equals(system)) {
295      CodeSystem cs = new CodeSystem();
296      cs.setUrl(system);
297      cs.setUserData("tx.cs.special", new URICodeSystem());
298      cs.setContent(CodeSystemContentMode.COMPLETE);
299      return cs; 
300    }
301    return null;
302  }
303
304  private ValidationResult findCodeInExpansion(Coding code) {
305    if (valueset==null || !valueset.hasExpansion())
306      return null;
307    return findCodeInExpansion(code, valueset.getExpansion().getContains());
308  }
309
310  private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
311    for (ValueSetExpansionContainsComponent containsComponent: contains) {
312      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
313        ConceptDefinitionComponent ccd = new ConceptDefinitionComponent();
314        ccd.setCode(containsComponent.getCode());
315        ccd.setDisplay(containsComponent.getDisplay());
316        ValidationResult res = new ValidationResult(code.getSystem(), ccd);
317        return res;
318      }
319      if (containsComponent.hasContains()) {
320        ValidationResult res = findCodeInExpansion(code, containsComponent.getContains());
321        if (res != null) {
322          return res;
323        }
324      }
325    }
326    return null;
327  }
328
329  private boolean checkExpansion(Coding code) {
330    if (valueset==null || !valueset.hasExpansion()) {
331      return false;
332    }
333    return checkExpansion(code, valueset.getExpansion().getContains());
334  }
335
336  private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
337    for (ValueSetExpansionContainsComponent containsComponent: contains) {
338      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
339        return true;
340      }
341      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) {
342        return true;
343      }
344    }
345    return false;
346  }
347
348  private ValidationResult validateCode(Coding code, CodeSystem cs) {
349    ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode());
350    if (cc == null) {
351      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
352        return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, gen(code), cs.getUrl()));        
353      } else {
354        return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, gen(code), cs.getUrl()));
355      }
356    }
357    if (code.getDisplay() == null) {
358      return new ValidationResult(code.getSystem(), cc);
359    }
360    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
361    if (cc.hasDisplay()) {
362      b.append(cc.getDisplay());
363      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
364        return new ValidationResult(code.getSystem(), cc);
365      }
366    }
367    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
368      b.append(ds.getValue());
369      if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
370        return new ValidationResult(code.getSystem(), cc);
371      }
372    }
373    // also check to see if the value set has another display
374    ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode());
375    if (vs != null && (vs.hasDisplay() ||vs.hasDesignation())) {
376      if (vs.hasDisplay()) {
377        b.append(vs.getDisplay());
378        if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) {
379          return new ValidationResult(code.getSystem(), cc);
380        }
381      }
382      for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) {
383        b.append(ds.getValue());
384        if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
385          return new ValidationResult(code.getSystem(), cc);
386        }
387      }
388    }
389    return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF_, code.getSystem(), code.getCode(), b.toString(), code.getDisplay()), code.getSystem(), cc);
390  }
391
392  private ConceptReferenceComponent findValueSetRef(String system, String code) {
393    if (valueset == null)
394      return null;
395    // if it has an expansion
396    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
397      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
398        ConceptReferenceComponent cc = new ConceptReferenceComponent();
399        cc.setDisplay(exp.getDisplay());
400        cc.setDesignation(exp.getDesignation());
401        return cc;
402      }
403    }
404    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
405      if (system.equals(inc.getSystem())) {
406        for (ConceptReferenceComponent cc : inc.getConcept()) {
407          if (cc.getCode().equals(code)) {
408            return cc;
409          }
410        }
411      }
412      for (CanonicalType url : inc.getValueSet()) {
413        ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code);
414        if (cc != null) {
415          return cc;
416        }
417      }
418    }
419    return null;
420  }
421
422  private String gen(Coding code) {
423    if (code.hasSystem()) {
424      return code.getSystem()+"#"+code.getCode();
425    } else {
426      return null;
427    }
428  }
429
430  private String getValueSetSystem() throws FHIRException {
431    if (valueset == null) {
432      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__NO_VALUE_SET));
433    }
434    if (valueset.getCompose().getInclude().size() == 0) {
435      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) {
436        throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION));
437      } else {
438        String cs = valueset.getExpansion().getContains().get(0).getSystem();
439        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) {
440          return cs;
441        } else {
442          throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_EXPANSION_HAS_MULTIPLE_SYSTEMS));
443        }
444      }
445    }
446    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
447      if (inc.hasValueSet()) {
448        throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_IMPORTS));
449      }
450      if (!inc.hasSystem()) {
451        throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM));
452      }
453    }
454    if (valueset.getCompose().getInclude().size() == 1) {
455      return valueset.getCompose().getInclude().get(0).getSystem();
456    }
457
458    return null;
459  }
460
461  private String getValueSetSystemOrNull() throws FHIRException {
462    if (valueset == null) {
463      return null;
464    }
465    if (valueset.getCompose().getInclude().size() == 0) {
466      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) {
467        return null;
468      } else {
469        String cs = valueset.getExpansion().getContains().get(0).getSystem();
470        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) {
471          return cs;
472        } else {
473          return null;
474        }
475      }
476    }
477    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
478      if (inc.hasValueSet()) {
479        return null;
480      }
481      if (!inc.hasSystem()) {
482        return null;
483      }
484    }
485    if (valueset.getCompose().getInclude().size() == 1) {
486      return valueset.getCompose().getInclude().get(0).getSystem();
487    }
488
489    return null;
490  }
491
492  /*
493   * Check that all system values within an expansion correspond to the specified system value
494   */
495  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
496    for (ValueSetExpansionContainsComponent contains : containsList) {
497      if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) {
498        return false;
499      }
500    }
501    return true;
502  }
503
504  private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code) {
505    if (code.equals(concept.getCode())) {
506      return concept;
507    }
508    ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code);
509    if (cc != null) {
510      return cc;
511    }
512    if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
513      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
514      for (ConceptDefinitionComponent c : children) {
515        cc = findCodeInConcept(c, code);
516        if (cc != null) {
517          return cc;
518        }
519      }
520    }
521    return null;
522  }
523  
524  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
525    for (ConceptDefinitionComponent cc : concept) {
526      if (code.equals(cc.getCode())) {
527        return cc;
528      }
529      ConceptDefinitionComponent c = findCodeInConcept(cc, code);
530      if (c != null) {
531        return c;
532      }
533    }
534    return null;
535  }
536
537
538  private String systemForCodeInValueSet(String code) {
539    String sys = null;
540    if (valueset.hasCompose()) {
541      if (valueset.getCompose().hasExclude()) {
542        return null;
543      }
544      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
545        if (vsi.hasValueSet()) {
546          return null;
547        }
548        if (!vsi.hasSystem()) { 
549          return null;
550        }
551        if (vsi.hasFilter()) {
552          return null;
553        }
554        CodeSystem cs = resolveCodeSystem(vsi.getSystem());
555        if (cs == null) {
556          return null;
557        }
558        if (vsi.hasConcept()) {
559          for (ConceptReferenceComponent cc : vsi.getConcept()) {
560            boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
561            if (match) {
562              if (sys == null) {
563                sys = vsi.getSystem();
564              } else if (!sys.equals(vsi.getSystem())) {
565                return null;
566              }
567            }
568          }
569        } else {
570          ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
571          if (cc != null) {
572            if (sys == null) {
573              sys = vsi.getSystem();
574            } else if (!sys.equals(vsi.getSystem())) {
575              return null;
576            }
577          }
578        }
579      }
580    } else if (valueset.hasExpansion()) {
581      // Retrieve a list of all systems associated with this code in the expansion
582      List<String> systems = new ArrayList<String>();
583      checkSystems(valueset.getExpansion().getContains(), code, systems);
584      if (systems.size()==1)
585        sys = systems.get(0);
586    }
587
588    return sys;  
589  }
590
591  /*
592   * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding
593   * to the passed list. 
594   */
595  private void checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, List<String> systems) {
596    for (ValueSetExpansionContainsComponent c: contains) {
597      if (c.getCode().equals(code)) {
598        if (!systems.contains(c.getSystem()))
599          systems.add(c.getSystem());
600      }
601      if (c.hasContains())
602        checkSystems(c.getContains(), code, systems);
603    }
604  }
605  
606  @Override
607  public Boolean codeInValueSet(String system, String code, List<String> warnings) throws FHIRException {
608    if (valueset == null) {
609      return false;
610    }
611    Boolean result = false;
612      
613    if (valueset.hasExpansion()) {
614      return checkExpansion(new Coding(system, code, null));
615    } else if (valueset.hasCompose()) {
616      int i = 0;
617      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
618        Boolean ok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings);
619        i++;
620        if (ok == null && result == false) {
621          result = null;
622        } else if (ok) {
623          result = true;
624          break;
625        }
626      }
627      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
628        Boolean nok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings);
629        i++;
630        if (nok == null && result == false) {
631          result = null;
632        } else if (nok != null && nok) {
633          result = false;
634        }
635      }
636    } 
637
638    return result;
639  }
640
641  private Boolean inComponent(ConceptSetComponent vsi, int vsiIndex, String system, String code, boolean only, List<String> warnings) throws FHIRException {
642    for (UriType uri : vsi.getValueSet()) {
643      if (inImport(uri.getValue(), system, code)) {
644        return true;
645      }
646    }
647
648    if (!vsi.hasSystem()) {
649      return false;
650    }
651    if (only && system == null) {
652      // whether we know the system or not, we'll accept the stated codes at face value
653      for (ConceptReferenceComponent cc : vsi.getConcept()) {
654        if (cc.getCode().equals(code)) {
655          return true;
656        }
657      }
658    }
659
660    if (!system.equals(vsi.getSystem()))
661      return false;
662    // ok, we need the code system
663    CodeSystem cs = resolveCodeSystem(system);
664    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
665      // make up a transient value set with
666      ValueSet vs = new ValueSet();
667      vs.setStatus(PublicationStatus.ACTIVE);
668      vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
669      vs.setVersion(valueset.getVersion());
670      vs.getCompose().addInclude(vsi);
671      ValidationResult res = context.validateCode(options.noClient(), new Coding(system, code, null), vs);
672      if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
673        if (warnings != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
674          warnings.add(context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system));
675        }
676        return null;
677      }
678      if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
679        throw new NoTerminologyServiceException();
680      }
681      return res.isOk();
682    } else {
683      if (vsi.hasFilter()) {
684        boolean ok = true;
685        for (ConceptSetFilterComponent f : vsi.getFilter()) {
686          if (!codeInFilter(cs, system, f, code)) {
687            ok = false;
688            break;
689          }
690        }
691        return ok;
692      }
693
694      List<ConceptDefinitionComponent> list = cs.getConcept();
695      boolean ok = validateCodeInConceptList(code, cs, list);
696      if (ok && vsi.hasConcept()) {
697        for (ConceptReferenceComponent cc : vsi.getConcept()) {
698          if (cc.getCode().equals(code)) { 
699            return true;
700          }
701        }
702        return false;
703      } else {
704        return ok;
705      }
706    }
707  }
708
709  private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException {
710    if ("concept".equals(f.getProperty()))
711      return codeInConceptFilter(cs, f, code);
712    else {
713      System.out.println("todo: handle filters with property = "+f.getProperty()); 
714      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty()));
715    }
716  }
717
718  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
719    switch (f.getOp()) {
720    case ISA: return codeInConceptIsAFilter(cs, f, code, false);
721    case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false);
722    case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 
723    default:
724      System.out.println("todo: handle concept filters with op = "+f.getOp()); 
725      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
726    }
727  }
728
729  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean rootOnly) {
730    if (!rootOnly && code.equals(f.getProperty())) {
731      return true;
732    }
733    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue());
734    if (cc == null) {
735      return false;
736    }
737    cc = findCodeInConcept(cc, code);
738    return cc != null;
739  }
740
741  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) {
742    if (def.getCaseSensitive()) {
743      for (ConceptDefinitionComponent cc : list) {
744        if (cc.getCode().equals(code)) { 
745          return true;
746        }
747        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) {
748          return true;
749        }
750      }
751    } else {
752      for (ConceptDefinitionComponent cc : list) {
753        if (cc.getCode().equalsIgnoreCase(code)) { 
754          return true;
755        }
756        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) {
757          return true;
758        }
759      }
760    }
761    return false;
762  }
763
764  private ValueSetCheckerSimple getVs(String url) {
765    if (inner.containsKey(url)) {
766      return inner.get(url);
767    }
768    ValueSet vs = context.fetchResource(ValueSet.class, url);
769    ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext);
770    inner.put(url, vsc);
771    return vsc;
772  }
773
774  private boolean inImport(String uri, String system, String code) throws FHIRException {
775    ValueSetCheckerSimple vs = getVs(uri);
776    if (vs == null) {
777      return false;
778    } else {
779      Boolean ok = vs.codeInValueSet(system, code, null);
780      return ok != null && ok;
781    }
782  }
783
784}