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            if (warnings != null) {
259              res.setMessage("Not in value set "+valueset.getUrl()+" ("+warnings+")").setSeverity(IssueSeverity.ERROR);              
260            } else {
261              res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR);
262            }
263          } else if (warningMessage!=null) {
264            res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage));
265          } else if (inExpansion) {
266            res.setMessage("Code found in expansion, however: " + res.getMessage());
267          } else if (inInclude) {
268            res.setMessage("Code found in include, however: " + res.getMessage());
269          }
270        }
271      }
272    }
273    return res;
274  }
275
276  private boolean checkInclude(Coding code) {
277    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
278      return false;
279    }
280    for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
281      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
282        for (ConceptReferenceComponent cc : inc.getConcept()) {
283          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
284            return false;
285          }
286        }
287      }
288    }
289    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
290      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
291        for (ConceptReferenceComponent cc : inc.getConcept()) {
292          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
293            return true;
294          }
295        }
296      }
297    }
298    return false;
299  }
300
301  private CodeSystem findSpecialCodeSystem(String system) {
302    if ("urn:ietf:rfc:3986".equals(system)) {
303      CodeSystem cs = new CodeSystem();
304      cs.setUrl(system);
305      cs.setUserData("tx.cs.special", new URICodeSystem());
306      cs.setContent(CodeSystemContentMode.COMPLETE);
307      return cs; 
308    }
309    return null;
310  }
311
312  private ValidationResult findCodeInExpansion(Coding code) {
313    if (valueset==null || !valueset.hasExpansion())
314      return null;
315    return findCodeInExpansion(code, valueset.getExpansion().getContains());
316  }
317
318  private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
319    for (ValueSetExpansionContainsComponent containsComponent: contains) {
320      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
321        ConceptDefinitionComponent ccd = new ConceptDefinitionComponent();
322        ccd.setCode(containsComponent.getCode());
323        ccd.setDisplay(containsComponent.getDisplay());
324        ValidationResult res = new ValidationResult(code.getSystem(), ccd);
325        return res;
326      }
327      if (containsComponent.hasContains()) {
328        ValidationResult res = findCodeInExpansion(code, containsComponent.getContains());
329        if (res != null) {
330          return res;
331        }
332      }
333    }
334    return null;
335  }
336
337  private boolean checkExpansion(Coding code) {
338    if (valueset==null || !valueset.hasExpansion()) {
339      return false;
340    }
341    return checkExpansion(code, valueset.getExpansion().getContains());
342  }
343
344  private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
345    for (ValueSetExpansionContainsComponent containsComponent: contains) {
346      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
347        return true;
348      }
349      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) {
350        return true;
351      }
352    }
353    return false;
354  }
355
356  private ValidationResult validateCode(Coding code, CodeSystem cs) {
357    ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode());
358    if (cc == null) {
359      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
360        return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, gen(code), cs.getUrl()));        
361      } else {
362        return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, gen(code), cs.getUrl()));
363      }
364    }
365    if (code.getDisplay() == null) {
366      return new ValidationResult(code.getSystem(), cc);
367    }
368    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
369    if (cc.hasDisplay()) {
370      b.append(cc.getDisplay());
371      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
372        return new ValidationResult(code.getSystem(), cc);
373      }
374    }
375    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
376      b.append(ds.getValue());
377      if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
378        return new ValidationResult(code.getSystem(), cc);
379      }
380    }
381    // also check to see if the value set has another display
382    ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode());
383    if (vs != null && (vs.hasDisplay() ||vs.hasDesignation())) {
384      if (vs.hasDisplay()) {
385        b.append(vs.getDisplay());
386        if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) {
387          return new ValidationResult(code.getSystem(), cc);
388        }
389      }
390      for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) {
391        b.append(ds.getValue());
392        if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
393          return new ValidationResult(code.getSystem(), cc);
394        }
395      }
396    }
397    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);
398  }
399
400  private ConceptReferenceComponent findValueSetRef(String system, String code) {
401    if (valueset == null)
402      return null;
403    // if it has an expansion
404    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
405      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
406        ConceptReferenceComponent cc = new ConceptReferenceComponent();
407        cc.setDisplay(exp.getDisplay());
408        cc.setDesignation(exp.getDesignation());
409        return cc;
410      }
411    }
412    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
413      if (system.equals(inc.getSystem())) {
414        for (ConceptReferenceComponent cc : inc.getConcept()) {
415          if (cc.getCode().equals(code)) {
416            return cc;
417          }
418        }
419      }
420      for (CanonicalType url : inc.getValueSet()) {
421        ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code);
422        if (cc != null) {
423          return cc;
424        }
425      }
426    }
427    return null;
428  }
429
430  private String gen(Coding code) {
431    if (code.hasSystem()) {
432      return code.getSystem()+"#"+code.getCode();
433    } else {
434      return null;
435    }
436  }
437
438  private String getValueSetSystem() throws FHIRException {
439    if (valueset == null) {
440      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__NO_VALUE_SET));
441    }
442    if (valueset.getCompose().getInclude().size() == 0) {
443      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) {
444        throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION));
445      } else {
446        String cs = valueset.getExpansion().getContains().get(0).getSystem();
447        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) {
448          return cs;
449        } else {
450          throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_EXPANSION_HAS_MULTIPLE_SYSTEMS));
451        }
452      }
453    }
454    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
455      if (inc.hasValueSet()) {
456        throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_IMPORTS));
457      }
458      if (!inc.hasSystem()) {
459        throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM));
460      }
461    }
462    if (valueset.getCompose().getInclude().size() == 1) {
463      return valueset.getCompose().getInclude().get(0).getSystem();
464    }
465
466    return null;
467  }
468
469  private String getValueSetSystemOrNull() throws FHIRException {
470    if (valueset == null) {
471      return null;
472    }
473    if (valueset.getCompose().getInclude().size() == 0) {
474      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) {
475        return null;
476      } else {
477        String cs = valueset.getExpansion().getContains().get(0).getSystem();
478        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) {
479          return cs;
480        } else {
481          return null;
482        }
483      }
484    }
485    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
486      if (inc.hasValueSet()) {
487        return null;
488      }
489      if (!inc.hasSystem()) {
490        return null;
491      }
492    }
493    if (valueset.getCompose().getInclude().size() == 1) {
494      return valueset.getCompose().getInclude().get(0).getSystem();
495    }
496
497    return null;
498  }
499
500  /*
501   * Check that all system values within an expansion correspond to the specified system value
502   */
503  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
504    for (ValueSetExpansionContainsComponent contains : containsList) {
505      if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) {
506        return false;
507      }
508    }
509    return true;
510  }
511
512  private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code) {
513    if (code.equals(concept.getCode())) {
514      return concept;
515    }
516    ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code);
517    if (cc != null) {
518      return cc;
519    }
520    if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
521      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
522      for (ConceptDefinitionComponent c : children) {
523        cc = findCodeInConcept(c, code);
524        if (cc != null) {
525          return cc;
526        }
527      }
528    }
529    return null;
530  }
531  
532  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
533    for (ConceptDefinitionComponent cc : concept) {
534      if (code.equals(cc.getCode())) {
535        return cc;
536      }
537      ConceptDefinitionComponent c = findCodeInConcept(cc, code);
538      if (c != null) {
539        return c;
540      }
541    }
542    return null;
543  }
544
545
546  private String systemForCodeInValueSet(String code) {
547    String sys = null;
548    if (valueset.hasCompose()) {
549      if (valueset.getCompose().hasExclude()) {
550        return null;
551      }
552      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
553        if (vsi.hasValueSet()) {
554          return null;
555        }
556        if (!vsi.hasSystem()) { 
557          return null;
558        }
559        if (vsi.hasFilter()) {
560          return null;
561        }
562        CodeSystem cs = resolveCodeSystem(vsi.getSystem());
563        if (cs == null) {
564          return null;
565        }
566        if (vsi.hasConcept()) {
567          for (ConceptReferenceComponent cc : vsi.getConcept()) {
568            boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
569            if (match) {
570              if (sys == null) {
571                sys = vsi.getSystem();
572              } else if (!sys.equals(vsi.getSystem())) {
573                return null;
574              }
575            }
576          }
577        } else {
578          ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
579          if (cc != null) {
580            if (sys == null) {
581              sys = vsi.getSystem();
582            } else if (!sys.equals(vsi.getSystem())) {
583              return null;
584            }
585          }
586        }
587      }
588    } else if (valueset.hasExpansion()) {
589      // Retrieve a list of all systems associated with this code in the expansion
590      List<String> systems = new ArrayList<String>();
591      checkSystems(valueset.getExpansion().getContains(), code, systems);
592      if (systems.size()==1)
593        sys = systems.get(0);
594    }
595
596    return sys;  
597  }
598
599  /*
600   * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding
601   * to the passed list. 
602   */
603  private void checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, List<String> systems) {
604    for (ValueSetExpansionContainsComponent c: contains) {
605      if (c.getCode().equals(code)) {
606        if (!systems.contains(c.getSystem()))
607          systems.add(c.getSystem());
608      }
609      if (c.hasContains())
610        checkSystems(c.getContains(), code, systems);
611    }
612  }
613  
614  @Override
615  public Boolean codeInValueSet(String system, String code, List<String> warnings) throws FHIRException {
616    if (valueset == null) {
617      return false;
618    }
619    Boolean result = false;
620      
621    if (valueset.hasExpansion()) {
622      return checkExpansion(new Coding(system, code, null));
623    } else if (valueset.hasCompose()) {
624      int i = 0;
625      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
626        Boolean ok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings);
627        i++;
628        if (ok == null && result == false) {
629          result = null;
630        } else if (ok) {
631          result = true;
632          break;
633        }
634      }
635      i = valueset.getCompose().getInclude().size();
636      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
637        Boolean nok = inComponent(vsi, i, system, code, valueset.getCompose().getInclude().size() == 1, warnings);
638        i++;
639        if (nok == null && result == false) {
640          result = null;
641        } else if (nok != null && nok) {
642          result = false;
643        }
644      }
645    } 
646
647    return result;
648  }
649
650  private Boolean inComponent(ConceptSetComponent vsi, int vsiIndex, String system, String code, boolean only, List<String> warnings) throws FHIRException {
651    for (UriType uri : vsi.getValueSet()) {
652      if (inImport(uri.getValue(), system, code)) {
653        return true;
654      }
655    }
656
657    if (!vsi.hasSystem()) {
658      return false;
659    }
660    if (only && system == null) {
661      // whether we know the system or not, we'll accept the stated codes at face value
662      for (ConceptReferenceComponent cc : vsi.getConcept()) {
663        if (cc.getCode().equals(code)) {
664          return true;
665        }
666      }
667    }
668
669    if (!system.equals(vsi.getSystem()))
670      return false;
671    // ok, we need the code system
672    CodeSystem cs = resolveCodeSystem(system);
673    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
674      // make up a transient value set with
675      ValueSet vs = new ValueSet();
676      vs.setStatus(PublicationStatus.ACTIVE);
677      vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
678      vs.setVersion(valueset.getVersion());
679      vs.getCompose().addInclude(vsi);
680      ValidationResult res = context.validateCode(options.noClient(), new Coding(system, code, null), vs);
681      if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
682        if (warnings != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
683          warnings.add(context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system));
684        }
685        return null;
686      }
687      if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
688        throw new NoTerminologyServiceException();
689      }
690      return res.isOk();
691    } else {
692      if (vsi.hasFilter()) {
693        boolean ok = true;
694        for (ConceptSetFilterComponent f : vsi.getFilter()) {
695          if (!codeInFilter(cs, system, f, code)) {
696            ok = false;
697            break;
698          }
699        }
700        return ok;
701      }
702
703      List<ConceptDefinitionComponent> list = cs.getConcept();
704      boolean ok = validateCodeInConceptList(code, cs, list);
705      if (ok && vsi.hasConcept()) {
706        for (ConceptReferenceComponent cc : vsi.getConcept()) {
707          if (cc.getCode().equals(code)) { 
708            return true;
709          }
710        }
711        return false;
712      } else {
713        return ok;
714      }
715    }
716  }
717
718  private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException {
719    if ("concept".equals(f.getProperty()))
720      return codeInConceptFilter(cs, f, code);
721    else {
722      System.out.println("todo: handle filters with property = "+f.getProperty()); 
723      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty()));
724    }
725  }
726
727  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
728    switch (f.getOp()) {
729    case ISA: return codeInConceptIsAFilter(cs, f, code, false);
730    case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false);
731    case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 
732    default:
733      System.out.println("todo: handle concept filters with op = "+f.getOp()); 
734      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
735    }
736  }
737
738  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean rootOnly) {
739    if (!rootOnly && code.equals(f.getProperty())) {
740      return true;
741    }
742    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue());
743    if (cc == null) {
744      return false;
745    }
746    cc = findCodeInConcept(cc, code);
747    return cc != null;
748  }
749
750  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) {
751    if (def.getCaseSensitive()) {
752      for (ConceptDefinitionComponent cc : list) {
753        if (cc.getCode().equals(code)) { 
754          return true;
755        }
756        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) {
757          return true;
758        }
759      }
760    } else {
761      for (ConceptDefinitionComponent cc : list) {
762        if (cc.getCode().equalsIgnoreCase(code)) { 
763          return true;
764        }
765        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) {
766          return true;
767        }
768      }
769    }
770    return false;
771  }
772
773  private ValueSetCheckerSimple getVs(String url) {
774    if (inner.containsKey(url)) {
775      return inner.get(url);
776    }
777    ValueSet vs = context.fetchResource(ValueSet.class, url);
778    ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext);
779    inner.put(url, vsc);
780    return vsc;
781  }
782
783  private boolean inImport(String uri, String system, String code) throws FHIRException {
784    ValueSetCheckerSimple vs = getVs(uri);
785    if (vs == null) {
786      return false;
787    } else {
788      Boolean ok = vs.codeInValueSet(system, code, null);
789      return ok != null && ok;
790    }
791  }
792
793}