001package org.hl7.fhir.r5.terminologies.validation;
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
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Calendar;
035import java.util.GregorianCalendar;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041
042
043import org.hl7.fhir.exceptions.FHIRException;
044import org.hl7.fhir.exceptions.NoTerminologyServiceException;
045import org.hl7.fhir.r5.context.ContextUtilities;
046import org.hl7.fhir.r5.context.IWorkerContext;
047import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
048import org.hl7.fhir.r5.elementmodel.LanguageUtils;
049import org.hl7.fhir.r5.extensions.ExtensionConstants;
050import org.hl7.fhir.r5.model.CanonicalType;
051import org.hl7.fhir.r5.model.CodeSystem;
052import org.hl7.fhir.r5.model.CodeType;
053import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode;
054import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
055import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
056import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
057import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
058import org.hl7.fhir.r5.model.CodeableConcept;
059import org.hl7.fhir.r5.model.Coding;
060import org.hl7.fhir.r5.model.DataType;
061import org.hl7.fhir.r5.model.Extension;
062import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
063import org.hl7.fhir.r5.model.OperationOutcome.IssueType;
064import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent;
065import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
066import org.hl7.fhir.r5.model.PackageInformation;
067import org.hl7.fhir.r5.model.Parameters;
068import org.hl7.fhir.r5.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent;
069import org.hl7.fhir.r5.model.TerminologyCapabilities;
070import org.hl7.fhir.r5.model.Transport.ParameterComponent;
071import org.hl7.fhir.r5.model.UriType;
072import org.hl7.fhir.r5.model.ValueSet;
073import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
074import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
075import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
076import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
077import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
078import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
079import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
080import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider;
081import org.hl7.fhir.r5.terminologies.providers.SpecialCodeSystem;
082import org.hl7.fhir.r5.terminologies.providers.URICodeSystem;
083import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext;
084import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException;
085import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass;
086import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase;
087import org.hl7.fhir.r5.utils.ToolingExtensions;
088import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
089import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
090import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
091import org.hl7.fhir.utilities.Utilities;
092import org.hl7.fhir.utilities.VersionUtilities;
093import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
094import org.hl7.fhir.utilities.i18n.I18nConstants;
095import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
096import org.hl7.fhir.utilities.validation.ValidationOptions;
097import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode;
098
099import com.google.j2objc.annotations.ReflectionSupport.Level;
100
101public class ValueSetValidator extends ValueSetProcessBase {
102
103  private ValueSet valueset;
104  private Map<String, ValueSetValidator> inner = new HashMap<>();
105  private ValidationOptions options;
106  private ValidationContextCarrier localContext;
107  private List<CodeSystem> localSystems = new ArrayList<>();
108  protected Parameters expansionProfile;
109  private TerminologyCapabilities txCaps;
110  private Set<String> unknownSystems;
111  private boolean throwToServer;
112
113  public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, Parameters expansionProfile, TerminologyCapabilities txCaps) {
114    super(context, opContext);
115    this.valueset = source;
116    this.options = options;
117    this.expansionProfile = expansionProfile;
118    this.txCaps = txCaps;
119    analyseValueSet();
120  }
121  
122  public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyCapabilities txCaps) {
123    super(context, opContext);
124    this.valueset = source;
125    this.options = options.copy();
126    this.options.setEnglishOk(true);
127    this.localContext = ctxt;
128    this.expansionProfile = expansionProfile;
129    this.txCaps = txCaps;
130    analyseValueSet();
131  }
132
133  public Set<String> getUnknownSystems() {
134    return unknownSystems;
135  }
136
137  public void setUnknownSystems(Set<String> unknownSystems) {
138    this.unknownSystems = unknownSystems;
139  }
140
141  public boolean isThrowToServer() {
142    return throwToServer;
143  }
144
145  public void setThrowToServer(boolean throwToServer) {
146    this.throwToServer = throwToServer;
147  }
148
149  private void analyseValueSet() {
150    if (valueset != null) {
151      opContext.seeContext(valueset.getVersionedUrl());
152      for (Extension s : valueset.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) {
153        requiredSupplements.add(s.getValue().primitiveValue());
154      }
155    }
156
157    altCodeParams.seeParameters(expansionProfile);
158    altCodeParams.seeValueSet(valueset);
159    if (localContext != null) {
160      if (valueset != null) {
161        for (ConceptSetComponent i : valueset.getCompose().getInclude()) {
162          analyseComponent(i);
163        }
164        for (ConceptSetComponent i : valueset.getCompose().getExclude()) {
165          analyseComponent(i);
166        }
167      }
168    }
169  }
170
171  private void analyseComponent(ConceptSetComponent i) {
172    opContext.deadCheck();
173    if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) {
174      String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM);
175      if (ref.startsWith("#")) {
176        String id = ref.substring(1);
177        for (ValidationContextResourceProxy t : localContext.getResources()) {
178          CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class);
179          if (cs != null) {
180            localSystems.add(cs);
181          }
182        }
183      } else {        
184        throw new Error("Not done yet #2: "+ref);
185      }
186    }    
187  }
188
189  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
190    return validateCode("CodeableConcept", code);
191  }
192  
193  public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException {
194    opContext.deadCheck();
195    checkValueSetOptions();
196
197    // first, we validate the codings themselves
198    ValidationProcessInfo info = new ValidationProcessInfo();
199
200    CodeableConcept vcc = new CodeableConcept();
201    
202    if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) {
203      int i = 0;
204      for (Coding c : code.getCoding()) {
205        if (!c.hasSystem()) {
206          info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE)));
207        }
208        VersionInfo vi = new VersionInfo(this);
209        checkExpansion(c, vi);
210        checkInclude(c, vi);
211        CodeSystem cs = resolveCodeSystem(c.getSystem(), vi.getVersion(c.getSystem(), c.getVersion()));
212        ValidationResult res = null;
213        if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) {
214          if (context.isNoTerminologyServer()) {
215            if (c.hasVersion()) {
216              String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, c.getSystem(), c.getVersion() , resolveCodeSystemVersions(c.getSystem()).toString());
217                unknownSystems.add(c.getSystem()+"|"+c.getVersion());
218              res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg)).setUnknownSystems(unknownSystems);
219            } else {
220              String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, c.getSystem(), c.getVersion());
221                unknownSystems.add(c.getSystem());
222              res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg)).setUnknownSystems(unknownSystems);
223            }
224          } else {
225            res = context.validateCode(options.withNoClient(), c, null);
226          }
227        } else {
228          c.setUserData("cs", cs);
229          res = validateCode(path+".coding["+i+"]", c, cs, vcc, info);
230        }
231        info.getIssues().addAll(res.getIssues());
232        i++;
233      }
234    }
235    Coding foundCoding = null;
236    String msg = null;
237    Boolean result = false;
238    if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
239      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ");
240      
241      for (Coding c : code.getCoding()) {
242        b.append("'"+c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode()+"'");
243        Boolean ok = codeInValueSet(path, c.getSystem(), c.getVersion(), c.getCode(), info);
244        if (ok == null && result != null && result == false) {
245          result = null;
246        } else if (ok != null && ok) {
247          result = true;
248          foundCoding = c;
249          if (options.getValueSetMode() == ValueSetMode.CHECK_MEMERSHIP_ONLY) {
250            vcc.addCoding().setSystem(c.getSystem()).setVersion(c.getVersion()).setCode(c.getCode());
251          }
252        }
253        if (ok == null || !ok) {
254          vcc.removeCoding(c.getSystem(), c.getVersion(), c.getCode());          
255        }
256      }
257      if (result == null) {
258        msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString());
259        info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, unknownSystems.isEmpty() ? IssueType.CODEINVALID : IssueType.NOTFOUND, path, msg));
260      } else if (!result) {
261        msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString());
262        info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg));
263      }
264    }
265    if (vcc.hasCoding() && code.hasText()) {
266      vcc.setText(code.getText());
267    }
268    if (!checkRequiredSupplements(info)) {
269      return new ValidationResult(IssueSeverity.ERROR, info.getIssues().get(info.getIssues().size()-1).getDetails().getText(), info.getIssues());
270    } else if (info.hasErrors()) {
271      ValidationResult res = new ValidationResult(IssueSeverity.ERROR, info.summary(), info.getIssues());
272      if (foundCoding != null) {
273        ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
274        cd.setDisplay(lookupDisplay(foundCoding));
275        res.setDefinition(cd);
276        res.setSystem(foundCoding.getSystem());
277        res.setVersion(foundCoding.hasVersion() ? foundCoding.getVersion() : ((CodeSystem) foundCoding.getUserData("cs")).getVersion());
278        res.setDisplay(cd.getDisplay());
279      }
280      res.setUnknownSystems(unknownSystems);
281      res.addCodeableConcept(vcc);
282      return res;
283    } else if (result == null) {
284      return new ValidationResult(IssueSeverity.WARNING, info.summary(), info.getIssues());
285    } else if (foundCoding == null) {
286      return new ValidationResult(IssueSeverity.ERROR, "Internal Error that should not happen", makeIssue(IssueSeverity.FATAL, IssueType.EXCEPTION, path, "Internal Error that should not happen"));
287    } else if (info.getIssues().size() > 0) {
288      String disp = lookupDisplay(foundCoding);
289      ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
290      cd.setDisplay(disp);
291      return new ValidationResult(IssueSeverity.WARNING, info.summary(), foundCoding.getSystem(), getVersion(foundCoding), cd, disp, info.getIssues()).addCodeableConcept(vcc);
292    } else {
293      ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
294      cd.setDisplay(lookupDisplay(foundCoding));
295      return new ValidationResult(foundCoding.getSystem(), getVersion(foundCoding), cd, getPreferredDisplay(cd, null)).addCodeableConcept(vcc);
296    }
297  }
298
299  private boolean checkRequiredSupplements(ValidationProcessInfo info) {
300    if (!requiredSupplements.isEmpty()) {
301      String msg= context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements));
302      throw new TerminologyServiceProtectionException(msg, TerminologyServiceErrorClass.BUSINESS_RULE, IssueType.NOTFOUND);
303    }
304    return requiredSupplements.isEmpty();
305  }
306
307  private boolean valueSetDependsOn(String system, String version) {
308    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
309      if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) {
310        return true;
311      }
312    }
313    return false;
314  }
315
316  private String getVersion(Coding c) {
317    if (c.hasVersion()) {
318      return c.getVersion();
319    } else if (c.hasUserData("cs")) {
320      return ((CodeSystem) c.getUserData("cs")).getVersion();
321    } else {
322      return null;
323    }
324  }
325
326  private String lookupDisplay(Coding c) {
327    CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion());
328    if (cs != null) {
329      ConceptDefinitionComponent cd = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null);
330      if (cd != null) {
331        return getPreferredDisplay(cd, cs); 
332      }
333    }
334    return null;
335  }
336
337  public CodeSystem resolveCodeSystem(String system, String version) {
338    for (CodeSystem t : localSystems) {
339      if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) {
340        return t;
341      }
342    }
343    CodeSystem cs = context.fetchSupplementedCodeSystem(system, version);
344    if (cs == null) {
345      cs = findSpecialCodeSystem(system, version);
346    }
347    return cs;
348  }
349
350  public List<String> resolveCodeSystemVersions(String system) {
351    List<String> res = new ArrayList<>();
352    for (CodeSystem t : localSystems) {
353      if (t.getUrl().equals(system) && t.hasVersion()) {
354        res.add(t.getVersion());
355      }
356    }
357    res.addAll(new ContextUtilities(context).fetchCodeSystemVersions(system));
358    return res;
359  }
360
361  private boolean versionsMatch(String versionTest, String versionActual) {
362    return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual);
363  }
364
365  public ValidationResult validateCode(Coding code) throws FHIRException {
366    return validateCode("Coding", code); 
367  }
368  
369  public ValidationResult validateCode(String path, Coding code) throws FHIRException {
370    opContext.deadCheck();
371    checkValueSetOptions();
372    
373    String warningMessage = null;
374    // first, we validate the concept itself
375
376    ValidationResult res = null;
377    boolean inExpansion = false;
378    boolean inInclude = false;
379    List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
380    ValidationProcessInfo info = new ValidationProcessInfo(issues);
381    VersionInfo vi = new VersionInfo(this);
382    checkCanonical(issues, path, valueset, valueset);
383
384    String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull();
385    if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) {
386      if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum)
387        List<String> problems = new ArrayList<>();
388        system = systemForCodeInValueSet(code.getCode(), problems);
389        if (system == null) {
390          if (problems.size() == 0) {
391            throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code
392          } else if (problems.size() == 1) {
393            return new ValidationResult(IssueSeverity.ERROR, problems.get(0), makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, problems.get(0)));
394          } else {
395            ValidationResult vr = new ValidationResult(IssueSeverity.ERROR, problems.toString(), null);
396            for (String s : problems) {
397              vr.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, s));
398            }
399            return vr;
400          }
401        }
402      }
403      if (!code.hasSystem()) {
404        if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) {
405          system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets
406        }
407        code.setSystem(system);
408      }
409      inExpansion = checkExpansion(code, vi);
410      inInclude = checkInclude(code, vi);
411      String wv = vi.getVersion(system, code.getVersion());
412      CodeSystem cs = resolveCodeSystem(system, wv);
413      if (cs == null) {
414        if (wv == null) {
415          warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system);
416          unknownSystems.add(system);
417        } else {
418          warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, wv, resolveCodeSystemVersions(system).toString());
419          unknownSystems.add(system+"|"+wv);
420        }
421        if (!inExpansion) {
422          if (valueset != null && valueset.hasExpansion()) {
423            String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION,
424                valueset.getUrl(), 
425                code.getSystem(), 
426                code.getCode().toString());
427            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path, msg));
428            throw new VSCheckerException(msg, issues);
429          } else {
430            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".system", warningMessage));
431            res = new ValidationResult(IssueSeverity.WARNING, warningMessage, issues);              
432            if (valueset == null) {
433              throw new VSCheckerException(warningMessage, issues);
434            } else {
435//              String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
436//              issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
437              // we don't do this yet
438              // throw new VSCheckerException(warningMessage, issues); 
439            }
440          }
441        }
442      } else {
443        checkCanonical(issues, path, cs, valueset);
444      }
445      if (cs != null && cs.hasSupplements()) {
446        String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl());
447        return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path, msg));        
448      }
449      if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
450        warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition is not complete";
451        if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment
452          throw new VSCheckerException(warningMessage, null, true);
453        }
454      }
455
456      if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) {
457        if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) {
458          if (inInclude) {
459            ConceptReferenceComponent cc = findInInclude(code);
460            if (cc != null) {
461              // we'll take it on faith
462              String disp = getPreferredDisplay(cc);
463              res = new ValidationResult(system, cs.getVersion(), new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(disp), disp);
464              res.addToMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct");
465              return res;
466            }
467          }
468          // we can't validate that here. 
469          throw new FHIRException("Unable to evaluate based on empty code system");
470        }
471        res = validateCode(path, code, cs, null, info);
472        res.setIssues(issues);
473      } else if (cs == null && valueset.hasExpansion() && inExpansion) {
474        // we just take the value set as face value then
475        res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay());
476        if (!preferServerSide(system)) {
477          res.addToMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")");
478        }
479      } else {
480        // well, we didn't find a code system - try the expansion? 
481        // disabled waiting for discussion
482        if (throwToServer) {
483          throw new FHIRException("No; try the server");
484        }
485      }
486    } else {
487      inExpansion = checkExpansion(code, vi);
488      inInclude = checkInclude(code, vi);
489    }
490    String wv = vi.getVersion(system, code.getVersion());
491    if (!checkRequiredSupplements(info)) {
492      return new ValidationResult(IssueSeverity.ERROR, issues.get(issues.size()-1).getDetails().getText(), issues);
493    }
494
495    
496    // then, if we have a value set, we check it's in the value set
497    if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
498      if ((res==null || res.isOk())) { 
499        Boolean ok = codeInValueSet(path, system, wv, code.getCode(), info);
500        if (ok == null || !ok) {
501          if (res == null) {
502            res = new ValidationResult((IssueSeverity) null, null, info.getIssues());
503          }
504          if (info.getErr() != null) {
505            res.setErrorClass(info.getErr());
506          }
507          if (ok == null) {
508            String m = "Unable to check whether the code is in the value set "+valueset.getVersionedUrl();
509            res.addToMessage(m);
510            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, path, m));
511            res.setUnknownSystems(unknownSystems);
512            res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
513            res.setErrorClass(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
514          } else if (!inExpansion && !inInclude) {
515//            if (!info.getIssues().isEmpty()) {
516//              res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR);              
517//              res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage()));
518//            } else
519//            {
520              String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
521              res.addToMessage(msg).setSeverity(IssueSeverity.ERROR);
522              res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg));
523              res.setDefinition(null);
524              res.setSystem(null);
525              res.setDisplay(null);
526              res.setUnknownSystems(unknownSystems);
527//            }
528          } else if (warningMessage!=null) {
529            String msg = context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage);
530            res = new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, msg));
531          } else if (inExpansion) {
532            res.setMessage("Code found in expansion, however: " + res.getMessage());
533            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage()));
534          } else if (inInclude) {
535            res.setMessage("Code found in include, however: " + res.getMessage());
536            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage()));
537          }
538        } else if (res == null) {
539          res = new ValidationResult(system, wv, null, null);
540        }
541      } else if ((res != null && !res.isOk())) {
542        String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
543        res.setMessage(res.getMessage()+"; "+msg);
544        res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg));
545      }
546    }
547    if (res != null && res.getSeverity() == IssueSeverity.INFORMATION) {
548      res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
549    }
550    return res;
551  }
552
553  private void checkValueSetOptions() {
554    if (valueset != null) {
555      for (Extension ext : valueset.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) {
556        var name = ext.getExtensionString("name");
557        var value = ext.getExtensionByUrl("value").getValue();
558        if ("displayLanguage".equals(name)) {
559          options.setLanguages(value.primitiveValue());
560        }
561      }
562      if (!options.hasLanguages() && valueset.hasLanguage()) {
563        options.addLanguage(valueset.getLanguage());
564      }
565    }
566  }
567
568  private static final Set<String> SERVER_SIDE_LIST = new HashSet<>(Arrays.asList("http://fdasis.nlm.nih.gov", "http://hl7.org/fhir/sid/ndc", "http://loinc.org", "http://snomed.info/sct", "http://unitsofmeasure.org", 
569      "http://unstats.un.org/unsd/methods/m49/m49.htm", "http://varnomen.hgvs.org", "http://www.nlm.nih.gov/research/umls/rxnorm", "https://www.usps.com/",
570      "urn:ietf:bcp:13","urn:ietf:bcp:47","urn:ietf:rfc:3986", "urn:iso:std:iso:3166","urn:iso:std:iso:4217", "urn:oid:1.2.36.1.2001.1005.17"));
571  
572  private boolean preferServerSide(String system) {
573    if (SERVER_SIDE_LIST.contains(system)) {
574      return true;
575    }
576  
577    if (txCaps != null) {
578      for (TerminologyCapabilitiesCodeSystemComponent tccs : txCaps.getCodeSystem()) {
579        if (system.equals(tccs.getUri())) {
580          return true;
581        }
582      }
583    }
584    return false;    
585  }
586
587  private boolean checkInclude(Coding code, VersionInfo vi) {
588    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
589      return false;
590    }
591    for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
592      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
593        for (ConceptReferenceComponent cc : inc.getConcept()) {
594          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
595            return false;
596          }
597        }
598      }
599    }
600    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
601      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
602        vi.setComposeVersion(inc.getVersion());
603        for (ConceptReferenceComponent cc : inc.getConcept()) {
604          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
605            return true;
606          }
607        }
608      }
609    }
610    return false;
611  }
612
613  private ConceptReferenceComponent findInInclude(Coding code) {
614    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
615      return null;
616    }
617    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
618      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
619        for (ConceptReferenceComponent cc : inc.getConcept()) {
620          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
621            return cc;
622          }
623        }
624      }
625    }
626    return null;
627  }
628
629  private CodeSystem findSpecialCodeSystem(String system, String version) {
630    if ("urn:ietf:rfc:3986".equals(system)) {
631      CodeSystem cs = new CodeSystem();
632      cs.setUrl(system);
633      cs.setUserData("tx.cs.special", new URICodeSystem());
634      cs.setContent(CodeSystemContentMode.COMPLETE);
635      return cs; 
636    }
637    return null;
638  }
639
640  private ValidationResult findCodeInExpansion(Coding code) {
641    if (valueset==null || !valueset.hasExpansion())
642      return null;
643    return findCodeInExpansion(code, valueset.getExpansion().getContains());
644  }
645
646  private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
647    for (ValueSetExpansionContainsComponent containsComponent: contains) {
648      opContext.deadCheck();
649      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
650        ConceptDefinitionComponent ccd = new ConceptDefinitionComponent();
651        ccd.setCode(containsComponent.getCode());
652        ccd.setDisplay(containsComponent.getDisplay());
653        ValidationResult res = new ValidationResult(code.getSystem(), code.hasVersion() ? code.getVersion() : containsComponent.getVersion(), ccd, getPreferredDisplay(ccd, null));
654        return res;
655      }
656      if (containsComponent.hasContains()) {
657        ValidationResult res = findCodeInExpansion(code, containsComponent.getContains());
658        if (res != null) {
659          return res;
660        }
661      }
662    }
663    return null;
664  }
665
666  private boolean checkExpansion(Coding code, VersionInfo vi) {
667    if (valueset==null || !valueset.hasExpansion()) {
668      return false;
669    }
670    return checkExpansion(code, valueset.getExpansion().getContains(), vi);
671  }
672
673  private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains, VersionInfo vi) {
674    for (ValueSetExpansionContainsComponent containsComponent: contains) {
675      opContext.deadCheck();
676      if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
677        vi.setExpansionVersion(containsComponent.getVersion());
678        return true;
679      }
680      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains(), vi)) {
681        return true;
682      }
683    }
684    return false;
685  }
686
687  private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) {
688    ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), allAltCodes);
689    if (cc == null) {
690      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
691        String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, code.getCode(), cs.getUrl());
692        return new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg));        
693      } else {
694        String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, code.getCode(), cs.getUrl());
695        return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg));
696      }
697    }
698    Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs));
699    if (vcc != null) {
700      vcc.addCoding(vc);
701    }
702
703    boolean inactive = (CodeSystemUtilities.isInactive(cs, cc));
704    String status = inactive ? (CodeSystemUtilities.getStatus(cs, cc)) : null;
705
706    boolean ws = false;     
707    if (code.getDisplay() == null) {
708      return new ValidationResult(code.getSystem(), cs.getVersion(), cc, vc.getDisplay()).setStatus(inactive, status);
709    }
710    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ", " or ");
711    if (cc.hasDisplay() && isOkLanguage(cs.getLanguage())) {
712      b.append("'"+cc.getDisplay()+"'"+(cs.hasLanguage() ? " ("+cs.getLanguage()+")" : ""));
713      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
714        return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
715      } else if (Utilities.normalize(code.getDisplay()).equals(Utilities.normalize(cc.getDisplay()))) {
716        ws = true;
717      }
718    }
719    
720    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
721      opContext.deadCheck();
722      if (isOkLanguage(ds.getLanguage())) {
723        b.append("'"+ds.getValue()+"'");
724        if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
725          return new ValidationResult(code.getSystem(),cs.getVersion(),  cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
726        }
727        if (Utilities.normalize(code.getDisplay()).equalsIgnoreCase(Utilities.normalize(ds.getValue()))) {
728          ws = true;
729        }
730      }
731    }
732    // also check to see if the value set has another display
733    if (options.isUseValueSetDisplays()) {
734      ConceptReferencePair vs = findValueSetRef(code.getSystem(), code.getCode());
735      if (vs != null && (vs.getCc().hasDisplay() ||vs.getCc().hasDesignation())) {
736        if (vs.getCc().hasDisplay() && isOkLanguage(vs.getValueset().getLanguage())) {
737          b.append("'"+vs.getCc().getDisplay()+"'");
738          if (code.getDisplay().equalsIgnoreCase(vs.getCc().getDisplay())) {
739            return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
740          }
741        }
742        for (ConceptReferenceDesignationComponent ds : vs.getCc().getDesignation()) {
743          opContext.deadCheck();
744          if (isOkLanguage(ds.getLanguage())) {
745            b.append("'"+ds.getValue()+"'");
746            if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
747              return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
748            }
749          }
750        }
751      }
752    }
753    if (b.count() == 0) {
754      String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary());
755      return new ValidationResult(IssueSeverity.WARNING, msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg)).setStatus(inactive, status);      
756    } else {
757      String msg = context.formatMessagePlural(b.count(), ws ? I18nConstants.DISPLAY_NAME_WS_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF : I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF, code.getSystem(), code.getCode(), b.toString(), code.getDisplay(), options.langSummary());
758      return new ValidationResult(dispWarningStatus(), msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(dispWarning(), IssueType.INVALID, path+".display", msg)).setStatus(inactive, status);
759    }
760  }
761
762  private IssueSeverity dispWarning() {
763    return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.ERROR; 
764  }
765  
766  private IssueSeverity dispWarningStatus() {
767    return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; // information -> error later
768  }
769
770  private boolean isOkLanguage(String language) {
771    if (!options.hasLanguages()) {
772      return true;
773    }
774    if (LanguageUtils.langsMatch(options.getLanguages(), language)) {
775      return true;
776    }
777    if (language == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US") || options.isEnglishOk())) {
778      return true;
779    }
780    return false;
781  }
782
783  private ConceptReferencePair findValueSetRef(String system, String code) {
784    if (valueset == null)
785      return null;
786    // if it has an expansion
787    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
788      opContext.deadCheck();
789      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
790        ConceptReferenceComponent cc = new ConceptReferenceComponent();
791        cc.setDisplay(exp.getDisplay());
792        cc.setDesignation(exp.getDesignation());
793        return new ConceptReferencePair(valueset, cc);
794      }
795    }
796    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
797      if (system.equals(inc.getSystem())) {
798        for (ConceptReferenceComponent cc : inc.getConcept()) {
799          if (cc.getCode().equals(code)) {
800            return new ConceptReferencePair(valueset, cc);
801          }
802        }
803      }
804      for (CanonicalType url : inc.getValueSet()) {
805        ConceptReferencePair cc = getVs(url.asStringValue()).findValueSetRef(system, code);
806        if (cc != null) {
807          return cc;
808        }
809      }
810    }
811    return null;
812  }
813
814  private String gen(Coding code) {
815    if (code.hasSystem()) {
816      return code.getSystem()+"#"+code.getCode();
817    } else {
818      return null;
819    }
820  }
821
822
823  private String getValueSetSystemOrNull() throws FHIRException {
824    if (valueset == null) {
825      return null;
826    }
827    if (valueset.getCompose().getInclude().size() == 0) {
828      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) {
829        return null;
830      } else {
831        String cs = valueset.getExpansion().getContains().get(0).getSystem();
832        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) {
833          return cs;
834        } else {
835          return null;
836        }
837      }
838    }
839    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
840      if (inc.hasValueSet()) {
841        return null;
842      }
843      if (!inc.hasSystem()) {
844        return null;
845      }
846    }
847    if (valueset.getCompose().getInclude().size() == 1) {
848      return valueset.getCompose().getInclude().get(0).getSystem();
849    }
850
851    return null;
852  }
853
854  /*
855   * Check that all system values within an expansion correspond to the specified system value
856   */
857  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
858    for (ValueSetExpansionContainsComponent contains : containsList) {
859      if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) {
860        return false;
861      }
862    }
863    return true;
864  }
865
866  private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, AlternateCodesProcessingRules altCodeRules) {
867    opContext.deadCheck();
868    if (code.equals(concept.getCode())) {
869      return concept;
870    }
871    ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, altCodeRules);
872    if (cc != null) {
873      return cc;
874    }
875    if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
876      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
877      for (ConceptDefinitionComponent c : children) {
878        cc = findCodeInConcept(c, code, altCodeRules);
879        if (cc != null) {
880          return cc;
881        }
882      }
883    }
884    return null;
885  }
886  
887  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, AlternateCodesProcessingRules altCodeRules) {
888    for (ConceptDefinitionComponent cc : concept) {
889      if (code.equals(cc.getCode())) {
890        return cc;
891      }
892      if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
893        return cc;
894      }
895      ConceptDefinitionComponent c = findCodeInConcept(cc, code, altCodeRules);
896      if (c != null) {
897        return c;
898      }
899    }
900    return null;
901  }
902
903
904  private List<String> alternateCodes(ConceptDefinitionComponent focus, AlternateCodesProcessingRules altCodeRules) {
905    List<String> codes = new ArrayList<>();
906    for (ConceptPropertyComponent p : focus.getProperty()) {
907      if ("alternateCode".equals(p.getCode()) && (altCodeRules.passes(p.getExtension())) && p.getValue().isPrimitive()) {
908        codes.add(p.getValue().primitiveValue());        
909      }
910    }
911    return codes;
912  }
913
914  
915  private String systemForCodeInValueSet(String code, List<String> problems) {
916    Set<String> sys = new HashSet<>();
917    if (!scanForCodeInValueSet(code, sys, problems)) {
918      return null;
919    }
920    if (sys.size() != 1) {
921      problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, sys.toString()));
922      return null;
923    } else {
924      return sys.iterator().next();
925    }
926  }
927  
928  private boolean scanForCodeInValueSet(String code, Set<String> sys, List<String> problems) {
929    if (valueset.hasCompose()) {
930      //  ignore excludes - they can't make any difference
931      if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) {
932        problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, valueset.getVersionedUrl()));
933      }
934
935      int i = 0;
936      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
937        opContext.deadCheck();
938        if (vsi.hasValueSet()) {
939          for (CanonicalType u : vsi.getValueSet()) {
940            if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) {
941              return false;
942            }
943          }
944        } else if (!vsi.hasSystem()) { 
945          problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i));
946          return false;
947        }
948        if (vsi.hasSystem()) {
949          if (vsi.hasFilter()) {
950            problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem()));
951            return false;
952          }
953          CodeSystemProvider csp = CodeSystemProvider.factory(vsi.getSystem());
954          if (csp != null) {
955            Boolean ok = csp.checkCode(code);
956            if (ok == null) {
957              problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE, valueset.getVersionedUrl(), vsi.getSystem()));
958              sys.add(vsi.getSystem());
959            } else if (ok) {
960              sys.add(vsi.getSystem());
961            }
962          } else {
963            CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion());
964            if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) {
965
966              if (vsi.hasConcept()) {
967                for (ConceptReferenceComponent cc : vsi.getConcept()) {
968                  boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
969                  if (match) {
970                    sys.add(vsi.getSystem());
971                  }
972                }
973              } else {
974                ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, allAltCodes);
975                if (cc != null) {
976                  sys.add(vsi.getSystem());
977                }
978              }
979            } else if (vsi.hasConcept()) {
980              for (ConceptReferenceComponent cc : vsi.getConcept()) {
981                boolean match = cc.getCode().equals(code);
982                if (match) {
983                  sys.add(vsi.getSystem());
984                }
985              }
986            } else {
987              // we'll try to expand this one then 
988              ValueSetExpansionOutcome vse = context.expandVS(vsi, false, false);
989              if (vse.isOk()) {
990                if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) {
991                  return false;
992                }
993              } else {
994                problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem(), vse.getAllErrors().toString()));              
995                return false;
996              }
997            }
998          }
999        }
1000        i++;
1001      }
1002    } else if (valueset.hasExpansion()) {
1003      // Retrieve a list of all systems associated with this code in the expansion
1004      if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) {
1005        return false;
1006      }
1007    }
1008    return true;
1009  }
1010
1011  private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<String> problems) {
1012    ValueSetValidator vs = getVs(uri);
1013    return vs.scanForCodeInValueSet(code, sys, problems);
1014  }
1015
1016  /*
1017   * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding
1018   * to the passed list. 
1019   */
1020  private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<String> problems) {
1021    for (ValueSetExpansionContainsComponent c: contains) {
1022      opContext.deadCheck();
1023      if (c.getCode().equals(code)) {
1024        systems.add(c.getSystem());
1025      }
1026      if (c.hasContains())
1027        checkSystems(c.getContains(), code, systems, problems);
1028    }
1029    return true;
1030  }
1031  
1032  public Boolean codeInValueSet(String path, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
1033    if (valueset == null) {
1034      return false;
1035    }
1036    opContext.deadCheck();
1037    checkCanonical(info.getIssues(), path, valueset, valueset);
1038    Boolean result = false;
1039    VersionInfo vi = new VersionInfo(this);
1040      
1041    if (valueset.hasExpansion()) {
1042      return checkExpansion(new Coding(system, code, null), vi);
1043    } else if (valueset.hasCompose()) {
1044      int i = 0;
1045      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
1046        Boolean ok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info);
1047        i++;
1048        if (ok == null && result != null && result == false) {
1049          result = null;
1050        } else if (ok != null && ok) {
1051          result = true;
1052          break;
1053        }
1054      }
1055      i = valueset.getCompose().getInclude().size();
1056      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
1057        Boolean nok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info);
1058        i++;
1059        if (nok == null && result != null && result == false) {
1060          result = null;
1061        } else if (nok != null && nok) {
1062          result = false;
1063        }
1064      }
1065    } 
1066
1067    return result;
1068  }
1069
1070  private Boolean inComponent(String path, ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException {
1071    opContext.deadCheck();
1072    boolean ok = true;
1073    
1074    if (vsi.hasValueSet()) {
1075      if (isValueSetUnionImports()) {
1076        ok = false;
1077        for (UriType uri : vsi.getValueSet()) {
1078          if (inImport(path, uri.getValue(), system, version, code, info)) {
1079            return true;
1080          }
1081        }
1082      } else {
1083        ok = inImport(path, vsi.getValueSet().get(0).getValue(), system, version, code, info);
1084        for (int i = 1; i < vsi.getValueSet().size(); i++) {
1085          UriType uri = vsi.getValueSet().get(i);
1086          ok = ok && inImport(path, uri.getValue(), system, version, code, info); 
1087        }
1088      }
1089    }
1090
1091    if (!vsi.hasSystem() || !ok) {
1092      return ok;
1093    }
1094    
1095    if (only && system == null) {
1096      // whether we know the system or not, we'll accept the stated codes at face value
1097      for (ConceptReferenceComponent cc : vsi.getConcept()) {
1098        if (cc.getCode().equals(code)) {
1099          return true;
1100        }
1101      }
1102    }
1103
1104    if (system == null || !system.equals(vsi.getSystem()))
1105      return false;
1106    // ok, we need the code system
1107    CodeSystem cs = resolveCodeSystem(system, version);
1108    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
1109      if (throwToServer) {
1110        // make up a transient value set with
1111        ValueSet vs = new ValueSet();
1112        vs.setStatus(PublicationStatus.ACTIVE);
1113        vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
1114        vs.setVersion(valueset.getVersion());
1115        vs.getCompose().addInclude(vsi);
1116        ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs);
1117        if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
1118          if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
1119            // server didn't know the code system either - we'll take it face value
1120            info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system)));
1121            for (ConceptReferenceComponent cc : vsi.getConcept()) {
1122              if (cc.getCode().equals(code)) {
1123                return true;
1124              }
1125            }
1126            info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
1127            return null;
1128          }
1129          return false;
1130        }
1131        if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
1132          throw new NoTerminologyServiceException();
1133        }
1134        return res.isOk();
1135      } else {
1136        if (unknownSystems != null) {
1137          if (version == null) {
1138            unknownSystems.add(system);
1139          } else {
1140            unknownSystems.add(system+"|"+version);          
1141          }
1142        }
1143        return null;
1144      }
1145    } else {
1146      checkCanonical(info.getIssues(), path, cs, valueset);
1147      if (valueset.getCompose().hasInactive() && !valueset.getCompose().getInactive()) {
1148        if (CodeSystemUtilities.isInactive(cs, code)) {
1149          return false;
1150        }
1151      }
1152      
1153      if (vsi.hasFilter()) {
1154        ok = true;
1155        for (ConceptSetFilterComponent f : vsi.getFilter()) {
1156          if (!codeInFilter(cs, system, f, code)) {
1157            return false;
1158          }
1159        }
1160      }
1161
1162      List<ConceptDefinitionComponent> list = cs.getConcept();
1163      ok = validateCodeInConceptList(code, cs, list, allAltCodes);
1164      if (ok && vsi.hasConcept()) {
1165        for (ConceptReferenceComponent cc : vsi.getConcept()) {
1166          if (cc.getCode().equals(code)) { 
1167            return true;
1168          }
1169        }
1170        return false;
1171      } else {
1172        // recheck that this is a valid alternate code
1173        ok = validateCodeInConceptList(code, cs, list, altCodeParams);
1174        return ok;
1175      }
1176    }
1177  }
1178
1179  protected boolean isValueSetUnionImports() {
1180    PackageInformation p = (PackageInformation) valueset.getSourcePackage();
1181    if (p != null) {
1182      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
1183    } else {
1184      return false;
1185    }
1186  }
1187
1188  private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException {
1189    if ("concept".equals(f.getProperty()))
1190      return codeInConceptFilter(cs, f, code);
1191    else if ("code".equals(f.getProperty()) && f.getOp() == FilterOperator.REGEX)
1192      return codeInRegexFilter(cs, f, code);
1193    else if (CodeSystemUtilities.hasPropertyDef(cs, f.getProperty())) {
1194      return codeInPropertyFilter(cs, f, code);
1195    } else {
1196      System.out.println("todo: handle filters with property = "+f.getProperty()+" "+f.getOp().toCode()); 
1197      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty(), f.getOp().toCode()));
1198    }
1199  }
1200
1201  private boolean codeInPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1202    switch (f.getOp()) {
1203    case EQUAL:
1204      if (f.getValue() == null) {
1205        return false;
1206      }
1207      DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1208      return d != null && f.getValue().equals(d.primitiveValue());
1209    case EXISTS: 
1210      return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null;
1211    case REGEX:
1212      if (f.getValue() == null) {
1213        return false;
1214      }
1215      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1216      return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue());
1217    default:
1218      System.out.println("todo: handle property filters with op = "+f.getOp()); 
1219      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1220    }
1221  }
1222
1223  private boolean codeInRegexFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1224    return code.matches(f.getValue());
1225  }
1226
1227  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
1228    switch (f.getOp()) {
1229    case ISA: return codeInConceptIsAFilter(cs, f, code, false);
1230    case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false);
1231    case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 
1232    default:
1233      System.out.println("todo: handle concept filters with op = "+f.getOp()); 
1234      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1235    }
1236  }
1237
1238  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean excludeRoot) {
1239    if (!excludeRoot && code.equals(f.getValue())) {
1240      return true;
1241    }
1242    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), altCodeParams);
1243    if (cc == null) {
1244      return false;
1245    }
1246    ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, altCodeParams);
1247    return cc2 != null && cc2 != cc;
1248  }
1249
1250  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list, AlternateCodesProcessingRules altCodeRules) {
1251    opContext.deadCheck();
1252    if (def.getCaseSensitive()) {
1253      for (ConceptDefinitionComponent cc : list) {
1254        if (cc.getCode().equals(code)) { 
1255          return true;
1256        }
1257        if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
1258          return true;
1259        }
1260        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) {
1261          return true;
1262        }
1263      }
1264    } else {
1265      for (ConceptDefinitionComponent cc : list) {
1266        if (cc.getCode().equalsIgnoreCase(code)) { 
1267          return true;
1268        }
1269        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) {
1270          return true;
1271        }
1272      }
1273    }
1274    return false;
1275  }
1276
1277  private ValueSetValidator getVs(String url) {
1278    if (inner.containsKey(url)) {
1279      return inner.get(url);
1280    }
1281    ValueSet vs = context.fetchResource(ValueSet.class, url, valueset);
1282    ValueSetValidator vsc = new ValueSetValidator(context, opContext.copy(), options, vs, localContext, expansionProfile, txCaps);
1283    vsc.setThrowToServer(throwToServer);
1284    inner.put(url, vsc);
1285    return vsc;
1286  }
1287
1288  private boolean inImport(String path, String uri, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
1289    ValueSetValidator vs = getVs(uri);
1290    if (vs == null) {
1291      return false;
1292    } else {
1293      Boolean ok = vs.codeInValueSet(path, system, version, code, info);
1294      return ok != null && ok;
1295    }
1296  }
1297
1298
1299  private String getPreferredDisplay(ConceptReferenceComponent cc) {
1300    if (!options.hasLanguages()) {
1301      return cc.getDisplay();
1302    }
1303    if (LanguageUtils.langsMatch(options.getLanguages(), valueset.getLanguage())) {
1304      return cc.getDisplay();
1305    }
1306    // if there's no language, we default to accepting the displays as (US) English
1307    if (valueset.getLanguage() == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
1308      return cc.getDisplay();
1309    }
1310    for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
1311      if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1312        return d.getValue();
1313      }
1314    }
1315    for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
1316      if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1317        return d.getValue();
1318      }
1319    }
1320    return cc.getDisplay();
1321  }
1322
1323
1324  private String getPreferredDisplay(ConceptDefinitionComponent cc, CodeSystem cs) {
1325    if (!options.hasLanguages()) {
1326      return cc.getDisplay();
1327    }
1328    if (cs != null && LanguageUtils.langsMatch(options.getLanguages(), cs.getLanguage())) {
1329      return cc.getDisplay();
1330    }
1331    // if there's no language, we default to accepting the displays as (US) English
1332    if ((cs == null || cs.getLanguage() == null) && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
1333      return cc.getDisplay();
1334    }
1335    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
1336      if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1337        return d.getValue();
1338      }
1339    }
1340    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
1341      if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1342        return d.getValue();
1343      }
1344    }
1345    return cc.getDisplay();
1346  }
1347
1348}