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