001package org.hl7.fhir.r5.terminologies.validation;
002
003import java.io.IOException;
004
005/*
006  Copyright (c) 2011+, HL7, Inc.
007  All rights reserved.
008
009  Redistribution and use in source and binary forms, with or without modification, 
010  are permitted provided that the following conditions are met:
011
012 * Redistributions of source code must retain the above copyright notice, this 
013     list of conditions and the following disclaimer.
014 * Redistributions in binary form must reproduce the above copyright notice, 
015     this list of conditions and the following disclaimer in the documentation 
016     and/or other materials provided with the distribution.
017 * Neither the name of HL7 nor the names of its contributors may be used to 
018     endorse or promote products derived from this software without specific 
019     prior written permission.
020
021  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
022  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
023  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
024  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
025  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
026  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
027  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
028  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
029  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
030  POSSIBILITY OF SUCH DAMAGE.
031
032 */
033
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Calendar;
037import java.util.GregorianCalendar;
038import java.util.HashMap;
039import java.util.HashSet;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043
044
045import org.hl7.fhir.exceptions.FHIRException;
046import org.hl7.fhir.exceptions.NoTerminologyServiceException;
047import org.hl7.fhir.r5.context.ContextUtilities;
048import org.hl7.fhir.r5.context.IWorkerContext;
049import org.hl7.fhir.r5.elementmodel.LanguageUtils;
050import org.hl7.fhir.r5.extensions.ExtensionConstants;
051import org.hl7.fhir.r5.model.CanonicalType;
052import org.hl7.fhir.r5.model.CodeSystem;
053import org.hl7.fhir.r5.model.CodeType;
054import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode;
055import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
056import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
057import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
058import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
059import org.hl7.fhir.r5.model.CodeableConcept;
060import org.hl7.fhir.r5.model.Coding;
061import org.hl7.fhir.r5.model.DataType;
062import org.hl7.fhir.r5.model.Extension;
063import org.hl7.fhir.r5.model.NamingSystem;
064import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
065import org.hl7.fhir.r5.model.OperationOutcome.IssueType;
066import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent;
067import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
068import org.hl7.fhir.r5.model.PackageInformation;
069import org.hl7.fhir.r5.model.Parameters;
070import org.hl7.fhir.r5.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent;
071import org.hl7.fhir.r5.model.TerminologyCapabilities;
072import org.hl7.fhir.r5.model.Transport.ParameterComponent;
073import org.hl7.fhir.r5.model.UriType;
074import org.hl7.fhir.r5.model.ValueSet;
075import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
076import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
077import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
078import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
079import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
080import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
081import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager;
082import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
083import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider;
084import org.hl7.fhir.r5.terminologies.providers.SpecialCodeSystem;
085import org.hl7.fhir.r5.terminologies.providers.URICodeSystem;
086import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext;
087import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException;
088import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase.OpIssueCode;
089import org.hl7.fhir.r5.terminologies.validation.ValueSetValidator.StringWithCode;
090import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass;
091import org.hl7.fhir.r5.terminologies.utilities.ValidationResult;
092import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase;
093import org.hl7.fhir.r5.utils.OperationOutcomeUtilities;
094import org.hl7.fhir.r5.utils.ToolingExtensions;
095import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
096import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
097import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
098import org.hl7.fhir.utilities.FhirPublication;
099import org.hl7.fhir.utilities.Utilities;
100import org.hl7.fhir.utilities.VersionUtilities;
101import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
102import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
103import org.hl7.fhir.utilities.i18n.I18nConstants;
104import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
105import org.hl7.fhir.utilities.validation.ValidationOptions;
106
107import com.google.j2objc.annotations.ReflectionSupport.Level;
108
109public class ValueSetValidator extends ValueSetProcessBase {
110
111  public static final String NO_TRY_THE_SERVER = "The local terminology server cannot handle this request";
112
113
114  public class StringWithCode {
115    private OpIssueCode code;
116    private String message;
117    protected StringWithCode(OpIssueCode code, String message) {
118      super();
119      this.code = code;
120      this.message = message;
121    }
122    public OpIssueCode getCode() {
123      return code;
124    }
125    public String getMessage() {
126      return message;
127    } 
128  }
129
130  private ValueSet valueset;
131  private Map<String, ValueSetValidator> inner = new HashMap<>();
132  private ValidationOptions options;
133  private ValidationContextCarrier localContext;
134  private List<CodeSystem> localSystems = new ArrayList<>();
135  protected Parameters expansionProfile;
136  private TerminologyClientManager tcm;
137  private Set<String> unknownSystems;
138  private Set<String> unknownValueSets = new HashSet<>();
139  private boolean throwToServer;
140
141  public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, Parameters expansionProfile, TerminologyClientManager tcm) {
142    super(context, opContext);
143    this.valueset = source;
144    this.options = options;
145    this.expansionProfile = expansionProfile;
146    this.tcm = tcm;
147    analyseValueSet();
148  }
149  
150  public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyClientManager tcm) {
151    super(context, opContext);
152    this.valueset = source;
153    this.options = options.copy();
154    this.options.setEnglishOk(true);
155    this.localContext = ctxt;
156    this.expansionProfile = expansionProfile;
157    this.tcm = tcm;
158    analyseValueSet();
159  }
160
161  public Set<String> getUnknownSystems() {
162    return unknownSystems;
163  }
164
165  public void setUnknownSystems(Set<String> unknownSystems) {
166    this.unknownSystems = unknownSystems;
167  }
168
169  public boolean isThrowToServer() {
170    return throwToServer;
171  }
172
173  public void setThrowToServer(boolean throwToServer) {
174    this.throwToServer = throwToServer;
175  }
176
177  private void analyseValueSet() {
178    opContext.note("analyse");
179    if (valueset != null) {
180      opContext.note("vs = "+valueset.getVersionedUrl());
181      opContext.seeContext(valueset.getVersionedUrl());
182      for (Extension s : valueset.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) {
183        requiredSupplements.add(s.getValue().primitiveValue());
184      }
185    } else {
186      opContext.note("vs = null");
187    }
188
189    altCodeParams.seeParameters(expansionProfile);
190    altCodeParams.seeValueSet(valueset);
191    if (localContext != null) {
192      if (valueset != null) {
193        for (ConceptSetComponent i : valueset.getCompose().getInclude()) {
194          analyseComponent(i, "inc"+i);
195        }
196        for (ConceptSetComponent i : valueset.getCompose().getExclude()) {
197          analyseComponent(i, "exc"+i);
198        }
199      }
200    }
201    opContext.note("analysed");
202  }
203
204  private void analyseComponent(ConceptSetComponent i, String name) {
205    opContext.deadCheck("analyse Component "+name);
206    if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) {
207      String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM);
208      if (ref.startsWith("#")) {
209        String id = ref.substring(1);
210        for (ValidationContextResourceProxy t : localContext.getResources()) {
211          CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class);
212          if (cs != null) {
213            localSystems.add(cs);
214          }
215        }
216      } else {        
217        throw new Error("Not done yet #2: "+ref);
218      }
219    }    
220  }
221
222  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
223    return validateCode("CodeableConcept", code);
224  }
225  
226  public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException {
227    opContext.deadCheck("validate "+code.toString());
228    checkValueSetOptions();
229
230    // first, we validate the codings themselves
231    ValidationProcessInfo info = new ValidationProcessInfo();
232    
233    if (throwToServer) {
234      checkValueSetLoad(info);
235    }
236
237    CodeableConcept vcc = new CodeableConcept();
238    List<ValidationResult> resList = new ArrayList<>();
239    
240    if (!options.isMembershipOnly()) {
241      int i = 0;
242      for (Coding c : code.getCoding()) {
243        if (!c.hasSystem() && !c.hasUserData("val.sys.error")) {
244          c.setUserData("val.sys.error", true);
245          info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".coding["+i+"]", context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), OpIssueCode.InvalidData, null));
246        } else {
247          VersionInfo vi = new VersionInfo(this);
248          checkExpansion(c, vi);
249          checkInclude(c, vi);
250          CodeSystem cs = resolveCodeSystem(c.getSystem(), vi.getVersion(c.getSystem(), c.getVersion()));
251          ValidationResult res = null;
252          if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) {
253            if (context.isNoTerminologyServer()) {
254              if (c.hasVersion()) {
255                String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, c.getSystem(), c.getVersion() , resolveCodeSystemVersions(c.getSystem()).toString());
256                unknownSystems.add(c.getSystem()+"|"+c.getVersion());
257                res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.NotFound, null)).setUnknownSystems(unknownSystems);
258              } else {
259                String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, c.getSystem(), c.getVersion());
260                unknownSystems.add(c.getSystem());
261                res = new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.NotFound, null)).setUnknownSystems(unknownSystems);
262              }
263            } else {
264              res = context.validateCode(options.withNoClient(), c, null);
265              if (res.isOk()) {
266                vcc.addCoding(new Coding().setCode(res.getCode()).setVersion(res.getVersion()).setSystem(res.getSystem()).setDisplay(res.getDisplay()));
267              }
268              for (OperationOutcomeIssueComponent iss : res.getIssues()) {
269                if (iss.getSeverity() == org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR && iss.getDetails().hasCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", "not-found")) {
270                  iss.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING);
271                  res.setSeverity(IssueSeverity.WARNING);
272                }
273                iss.resetPath("Coding", path+".coding["+i+"]");
274              }
275              if (res.isInactive()) {
276                String msg = context.formatMessage(I18nConstants.STATUS_CODE_WARNING_CODE, "not active", c.getCode());
277                res.getIssues().addAll(makeIssue(IssueSeverity.INFORMATION, IssueType.INVALID, path+".coding["+i+"].code", msg, OpIssueCode.CodeRule, res.getServer()));
278              }
279            }
280          } else {
281            c.setUserData("cs", cs);
282
283            checkCanonical(info.getIssues(), path, cs, valueset);
284            res = validateCode(path+".coding["+i+"]", c, cs, vcc, info);
285          }
286          info.getIssues().addAll(res.getIssues());
287          if (res != null) {
288            resList.add(res);
289            if (!res.isOk() && !res.messageIsInIssues()) {
290              if (res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {                
291                info.getIssues().addAll(makeIssue(res.getSeverity(), IssueType.NOTFOUND, path+".coding["+i+"]", res.getMessage(), OpIssueCode.NotFound, res.getServer()));
292              } else {
293                info.getIssues().addAll(makeIssue(res.getSeverity(), IssueType.CODEINVALID, path+".coding["+i+"]", res.getMessage(), OpIssueCode.InvalidCode, res.getServer()));
294              }
295            }
296          } 
297        }
298        i++;
299      }
300    }
301    Coding foundCoding = null;
302    String msg = null;
303    Boolean result = false;
304    if (valueset != null) {
305      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ");
306      List<String> cpath = new ArrayList<>();
307      
308      int i = 0;
309      for (Coding c : code.getCoding()) {
310        String cs = "'"+c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode()+(c.hasDisplay() ? " ('"+c.getDisplay()+"')" : "")+"'";
311        String cs2 = c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "");
312        cpath.add(path+".coding["+i+"]");
313        b.append(cs2);
314        Boolean ok = codeInValueSet(path, c.getSystem(), c.getVersion(), c.getCode(), info);
315        if (ok == null && result != null && result == false) {
316          result = null;
317        } else if (ok != null && ok) {
318          result = true;
319          foundCoding = c;
320          if (!options.isMembershipOnly()) {
321            vcc.addCoding().setSystem(c.getSystem()).setVersion(c.getVersion()).setCode(c.getCode());
322          }
323        }
324        if (ok == null || !ok) {
325          vcc.removeCoding(c.getSystem(), c.getVersion(), c.getCode());          
326        }
327        if (ok != null && !ok) {
328          msg = context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE, null, valueset.getVersionedUrl(), cs);
329          info.getIssues().addAll(makeIssue(IssueSeverity.INFORMATION, IssueType.CODEINVALID, path+".coding["+i+"].code", msg, OpIssueCode.ThisNotInVS, null));
330        }
331        i++;
332      }
333      if (result == null) {
334        if (!unknownValueSets.isEmpty()) {
335          msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(", ", unknownValueSets));
336        } else {
337          msg = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS, valueset.getVersionedUrl(), b.toString());
338        }
339        info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, unknownSystems.isEmpty() && unknownValueSets.isEmpty() ? IssueType.CODEINVALID : IssueType.NOTFOUND, null, msg, OpIssueCode.VSProcessing, null));
340      } else if (!result) {
341        // to match Ontoserver
342        OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR, org.hl7.fhir.r5.model.OperationOutcome.IssueType.CODEINVALID);
343        iss.getDetails().setText(context.formatMessage(I18nConstants.TX_GENERAL_CC_ERROR_MESSAGE, valueset.getVersionedUrl()));
344        iss.getDetails().addCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", OpIssueCode.NotInVS.toCode(), null);
345        info.getIssues().add(iss);
346
347//        msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString());
348//        info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, code.getCoding().size() == 1 ? path+".coding[0].code" : path, msg));
349      }
350    }
351
352    if (vcc.hasCoding() && code.hasText()) {
353      vcc.setText(code.getText());
354    }
355    if (!checkRequiredSupplements(info)) {
356      return new ValidationResult(IssueSeverity.ERROR, info.getIssues().get(info.getIssues().size()-1).getDetails().getText(), info.getIssues());
357    } else if (info.hasErrors()) {
358      ValidationResult res = new ValidationResult(IssueSeverity.ERROR, info.summary(), info.getIssues());
359      if (foundCoding != null) {
360        ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
361        cd.setDisplay(lookupDisplay(foundCoding));
362        res.setDefinition(cd);
363        res.setSystem(foundCoding.getSystem());
364        res.setVersion(foundCoding.hasVersion() ? foundCoding.getVersion() : foundCoding.hasUserData("cs") ? ((CodeSystem) foundCoding.getUserData("cs")).getVersion() : null);
365        res.setDisplay(cd.getDisplay());
366      }
367      if (info.getErr() != null) {
368        res.setErrorClass(info.getErr());
369      }
370      res.setUnknownSystems(unknownSystems);
371      res.addCodeableConcept(vcc);
372      return res;
373    } else if (result == null) {
374      return new ValidationResult(IssueSeverity.WARNING, info.summary(), info.getIssues());
375    } else if (foundCoding == null && valueset != null) {
376      return new ValidationResult(IssueSeverity.ERROR, "Internal Error that should not happen", makeIssue(IssueSeverity.FATAL, IssueType.EXCEPTION, path, "Internal Error that should not happen", OpIssueCode.VSProcessing, null));
377    } else if (info.getIssues().size() > 0) {
378      if (foundCoding == null) {
379        IssueSeverity lvl = IssueSeverity.INFORMATION; 
380        for (OperationOutcomeIssueComponent iss : info.getIssues()) {
381          lvl = IssueSeverity.max(lvl, OperationOutcomeUtilities.convert(iss.getSeverity()));
382        }
383        return new ValidationResult(lvl, info.summary(), info.getIssues());        
384      } else {
385        String disp = lookupDisplay(foundCoding);
386        ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
387        cd.setDisplay(disp);
388        return new ValidationResult(IssueSeverity.WARNING, info.summaryList(), foundCoding.getSystem(), getVersion(foundCoding), cd, disp, info.getIssues()).addCodeableConcept(vcc);
389      }
390    } else if (!result) {
391      if (valueset != null) {
392        throw new Error("what?");
393      } else if (vcc.hasCoding()) {
394        return new ValidationResult(vcc.getCodingFirstRep().getSystem(), getVersion(vcc.getCodingFirstRep()), new ConceptDefinitionComponent(vcc.getCodingFirstRep().getCode()).setDisplay(vcc.getCodingFirstRep().getDisplay()), vcc.getCodingFirstRep().getDisplay()).addCodeableConcept(vcc);
395      } else {
396        throw new Error("what?");
397      }
398    } else if (foundCoding != null) {
399      ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
400      cd.setDisplay(lookupDisplay(foundCoding));
401      return new ValidationResult(foundCoding.getSystem(), getVersion(foundCoding), cd, getPreferredDisplay(cd, null)).addCodeableConcept(vcc);
402    } else {
403      throw new Error("what?");
404    }
405  }
406
407  private void checkValueSetLoad(ValidationProcessInfo info) {
408    int serverCount = getServerLoad(info);
409    // There's a trade off here: if we're going to hit the server inside the components, then
410    // the amount of value set collateral we send is limited, but we pay the price of hitting 
411    // the server multiple times. If, on the other hand, we give up on that, and hit the server 
412    // directly, we have to send value set collateral (though we cache at the higher level)
413    //
414    // the cutoff value is chosen experimentally
415    if (serverCount > 2) {
416      throw new VSCheckerException("This value set is better processed on the server for performance reasons", null, true);
417    }
418  }
419
420  private int getServerLoad(ValidationProcessInfo info) {
421    int serverCount = 0;
422    if (valueset != null) {
423      for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
424        serverCount = serverCount + checkValueSetLoad(inc, info);
425      }
426      for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
427        serverCount = serverCount + checkValueSetLoad(inc, info);
428      }
429    }
430    return serverCount;
431  }
432  
433  private int checkValueSetLoad(ConceptSetComponent inc, ValidationProcessInfo info) {
434    int serverCount = 0;
435    for (UriType uri : inc.getValueSet()) {
436      ValueSetValidator vsv = getVs(uri.getValue(), info);
437      serverCount += vsv.getServerLoad(info);
438    }
439    CodeSystem cs = resolveCodeSystem(inc.getSystem(), inc.getVersion());
440    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
441      serverCount++;
442    }
443    return serverCount;
444  }
445
446  private boolean checkRequiredSupplements(ValidationProcessInfo info) {
447    if (!requiredSupplements.isEmpty()) {
448      String msg= context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements));
449      throw new TerminologyServiceProtectionException(msg, TerminologyServiceErrorClass.BUSINESS_RULE, IssueType.NOTFOUND);
450    }
451    return requiredSupplements.isEmpty();
452  }
453
454  private boolean valueSetDependsOn(String system, String version) {
455    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
456      if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) {
457        return true;
458      }
459    }
460    return false;
461  }
462
463  private String getVersion(Coding c) {
464    if (c.hasVersion()) {
465      return c.getVersion();
466    } else if (c.hasUserData("cs")) {
467      return ((CodeSystem) c.getUserData("cs")).getVersion();
468    } else {
469      return null;
470    }
471  }
472
473  private String lookupDisplay(Coding c) {
474    CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion());
475    if (cs != null) {
476      ConceptDefinitionComponent cd = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null);
477      if (cd != null) {
478        return getPreferredDisplay(cd, cs); 
479      }
480    }
481    return null;
482  }
483
484  public CodeSystem resolveCodeSystem(String system, String version) {
485    for (CodeSystem t : localSystems) {
486      if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) {
487        return t;
488      }
489    }
490    CodeSystem cs = context.fetchSupplementedCodeSystem(system, version);
491    if (cs == null) {
492      cs = findSpecialCodeSystem(system, version);
493    }
494    return cs;
495  }
496
497  public List<String> resolveCodeSystemVersions(String system) {
498    List<String> res = new ArrayList<>();
499    for (CodeSystem t : localSystems) {
500      if (t.getUrl().equals(system) && t.hasVersion()) {
501        res.add(t.getVersion());
502      }
503    }
504    res.addAll(new ContextUtilities(context).fetchCodeSystemVersions(system));
505    return res;
506  }
507
508  private boolean versionsMatch(String versionTest, String versionActual) {
509    return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual);
510  }
511
512  public ValidationResult validateCode(Coding code) throws FHIRException {
513    return validateCode("Coding", code); 
514  }
515  
516  public ValidationResult validateCode(String path, Coding code) throws FHIRException {
517    opContext.deadCheck("validate "+code.toString());
518    checkValueSetOptions();
519    
520    String warningMessage = null;
521    // first, we validate the concept itself
522
523    ValidationResult res = null;
524    boolean inExpansion = false;
525    boolean inInclude = false;
526    List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
527    ValidationProcessInfo info = new ValidationProcessInfo(issues);
528    VersionInfo vi = new VersionInfo(this);
529    checkCanonical(issues, path, valueset, valueset);
530
531    String system = code.getSystem();
532    if (!options.isMembershipOnly()) {
533      if (system == null && !code.hasDisplay() && options.isGuessSystem()) { // dealing with just a plain code (enum)
534        List<StringWithCode> problems = new ArrayList<>();
535        system = systemForCodeInValueSet(code.getCode(), problems);
536        if (system == null) {
537          if (problems.size() == 0) {
538            throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code
539          } else if (problems.size() == 1) {
540            String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
541            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, "code", msg, OpIssueCode.NotInVS, null));
542            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, "code", problems.get(0).getMessage(), problems.get(0).getCode(), null));
543            return new ValidationResult(IssueSeverity.ERROR, problems.get(0).getMessage()+"; "+msg, issues);
544          } else {
545            CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("; ");
546            for (StringWithCode s : problems) {
547              b.append(s.getMessage());
548            }            
549            ValidationResult vr = new ValidationResult(IssueSeverity.ERROR, b.toString(), null);
550            for (StringWithCode s : problems) {
551              vr.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, s.getMessage(), s.getCode(), vr.getServer()));
552            }
553            return vr;
554          }
555        }
556      }
557      if (!code.hasSystem()) {
558        if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) {
559          system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets
560        }
561        code.setSystem(system);
562      }
563      if (!code.hasSystem()) {
564        res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), null);
565        if (!code.hasUserData("val.sys.error")) {
566          code.setUserData("val.sys.error", true);
567          res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), OpIssueCode.InvalidData, null));
568        }
569      } else {
570        if (!Utilities.isAbsoluteUrl(system)) {
571          String msg = context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_RELATIVE);
572          issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".system", msg, OpIssueCode.InvalidData, null));                
573        }
574        inExpansion = checkExpansion(code, vi);
575        inInclude = checkInclude(code, vi);
576        String wv = vi.getVersion(system, code.getVersion());
577        CodeSystem cs = resolveCodeSystem(system, wv);
578        if (cs == null) {
579          OpIssueCode oic = OpIssueCode.NotFound;
580          IssueType itype = IssueType.NOTFOUND;
581          ValueSet vs = context.fetchResource(ValueSet.class, system);
582          if (vs != null) {
583            warningMessage = context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_VALUESET2, system);  
584            oic = OpIssueCode.InvalidData;
585            itype = IssueType.INVALID;
586          } else if (wv == null) {
587            warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system);
588            unknownSystems.add(system);
589          } else {
590            warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, wv, resolveCodeSystemVersions(system).toString());
591            unknownSystems.add(system+"|"+wv);
592          }
593          if (!inExpansion) {
594            if (valueset != null && valueset.hasExpansion()) {
595              String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION,
596                  valueset.getUrl(), 
597                  code.getSystem(), 
598                  code.getCode().toString());
599              issues.addAll(makeIssue(IssueSeverity.ERROR, itype, path, msg, OpIssueCode.VSProcessing, null));
600              throw new VSCheckerException(msg, issues, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
601            } else {
602              issues.addAll(makeIssue(IssueSeverity.ERROR, itype, path+".system", warningMessage, oic, null));
603              res = new ValidationResult(IssueSeverity.WARNING, warningMessage, issues);              
604              if (valueset == null) {
605                throw new VSCheckerException(warningMessage, issues, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
606              } else {
607                //              String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
608                //              issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
609                // we don't do this yet
610                // throw new VSCheckerException(warningMessage, issues); 
611              }
612            }
613          }
614        } else {
615          checkCanonical(issues, path, cs, valueset);
616        }
617        if (cs != null && cs.hasSupplements()) {
618          String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl());
619          return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".system", msg, OpIssueCode.InvalidData, null));        
620        }
621        if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
622          warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition ";
623          switch (cs.getContent()) {
624          case EXAMPLE:
625            warningMessage = warningMessage +"only has example content";
626            break;
627          case FRAGMENT:
628            warningMessage = warningMessage + "is only a fragment";
629            break;
630          case NOTPRESENT:
631            warningMessage = warningMessage + "doesn't include any codes";
632            break;
633          case SUPPLEMENT:
634            warningMessage = warningMessage + " is for a supplement to "+cs.getSupplements();
635            break;
636          default:
637            break;
638          }
639          warningMessage = warningMessage + ", so the code has not been validated";
640          if (!options.isExampleOK() && !inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment
641            throw new VSCheckerException(warningMessage, null, true);
642          }
643        }
644
645        if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) {
646          if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT ||
647              (options.isExampleOK() && cs.getContent() == CodeSystemContentMode.EXAMPLE))) {
648            if (inInclude) {
649              ConceptReferenceComponent cc = findInInclude(code);
650              if (cc != null) {
651                // we'll take it on faith
652                String disp = getPreferredDisplay(cc);
653                res = new ValidationResult(system, cs.getVersion(), new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(disp), disp);
654                res.addMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct");
655                return res;
656              }
657            }
658            // we can't validate that here. 
659            throw new FHIRException("Unable to evaluate based on code system with status = "+cs.getContent().toCode());
660          }
661          res = validateCode(path, code, cs, null, info);
662          res.setIssues(issues);
663        } else if (cs == null && valueset.hasExpansion() && inExpansion) {
664          // we just take the value set as face value then
665          res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay());
666          if (!preferServerSide(system)) {
667            res.addMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")");
668          }
669        } else {
670          // well, we didn't find a code system - try the expansion? 
671          // disabled waiting for discussion
672          if (throwToServer) {
673            throw new FHIRException(NO_TRY_THE_SERVER);
674          }
675        }
676      }
677    } else {
678      inExpansion = checkExpansion(code, vi);
679      inInclude = checkInclude(code, vi);
680    }
681    String wv = vi.getVersion(system, code.getVersion());
682    if (!checkRequiredSupplements(info)) {
683      return new ValidationResult(IssueSeverity.ERROR, issues.get(issues.size()-1).getDetails().getText(), issues);
684    }
685
686    
687    // then, if we have a value set, we check it's in the value set
688    if (valueset != null) {
689      if ((res==null || res.isOk())) { 
690        Boolean ok = codeInValueSet(path, system, wv, code.getCode(), info);
691        if (ok == null || !ok) {
692          if (res == null) {
693            res = new ValidationResult((IssueSeverity) null, null, info.getIssues());
694          }
695          if (info.getErr() != null) {
696            res.setErrorClass(info.getErr());
697          }
698          if (ok == null) {
699            String m = null;
700            if (!unknownSystems.isEmpty()) {
701              m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(",", unknownSystems));
702            } else if (!unknownValueSets.isEmpty()) {
703              res.addMessage(info.getIssues().get(0).getDetails().getText());
704              m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(",", unknownValueSets));
705            } else {
706              // not sure why we'd get to here?
707              m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl());
708            }
709            res.addMessage(m);
710            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, null, m, OpIssueCode.VSProcessing, null));
711            res.setUnknownSystems(unknownSystems);
712            res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
713            res.setErrorClass(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
714          } else if (!inExpansion && !inInclude) {
715//            if (!info.getIssues().isEmpty()) {
716//              res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR);              
717//              res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage()));
718//            } else
719//            {
720              String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
721              res.addMessage(msg).setSeverity(IssueSeverity.ERROR);
722              res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.NotInVS, null));
723              res.setDefinition(null);
724              res.setSystem(null);
725              res.setDisplay(null);
726              res.setUnknownSystems(unknownSystems);              
727//            }
728          } else if (warningMessage!=null) {
729            String msg = context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage);
730            res = new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, msg, OpIssueCode.VSProcessing, null));
731          } else if (inExpansion) {
732            res.setMessage("Code found in expansion, however: " + res.getMessage());
733            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage(), OpIssueCode.VSProcessing, null));
734          } else if (inInclude) {
735            res.setMessage("Code found in include, however: " + res.getMessage());
736            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage(), OpIssueCode.VSProcessing, null));
737          }
738        } else if (res == null) {
739          res = new ValidationResult(system, wv, null, null);
740        }
741      } else if ((res != null && !res.isOk())) {
742        String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
743        res.addMessage(msg);
744        res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.NotInVS, null));
745      }
746    }
747    if (res != null && res.getSeverity() == IssueSeverity.INFORMATION && res.getMessage() != null) {
748      res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
749    }
750    return res;
751  }
752
753  private void checkValueSetOptions() {
754    if (valueset != null) {
755      for (Extension ext : valueset.getCompose().getExtensionsByUrl("http://hl7.org/fhir/tools/StructureDefinion/valueset-expansion-param")) {
756        var name = ext.getExtensionString("name");
757        var value = ext.getExtensionByUrl("value").getValue();
758        if ("displayLanguage".equals(name)) {
759          options.setLanguages(value.primitiveValue());
760        }
761      }
762      if (!options.hasLanguages() && valueset.hasLanguage()) {
763        options.addLanguage(valueset.getLanguage());
764      }
765    }
766  }
767
768  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", 
769      "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/",
770      "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"));
771  
772  private boolean preferServerSide(String system) {
773    if (SERVER_SIDE_LIST.contains(system)) {
774      return true;
775    }
776    
777    try {
778      if (tcm.supportsSystem(system)) {
779        return true;
780      }
781    } catch (IOException e) {
782      e.printStackTrace();
783    }
784  
785    return false;    
786  }
787
788  private boolean checkInclude(Coding code, VersionInfo vi) {
789    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
790      return false;
791    }
792    for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
793      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
794        for (ConceptReferenceComponent cc : inc.getConcept()) {
795          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
796            return false;
797          }
798        }
799      }
800    }
801    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
802      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
803        vi.setComposeVersion(inc.getVersion());
804        for (ConceptReferenceComponent cc : inc.getConcept()) {
805          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
806            return true;
807          }
808        }
809      }
810    }
811    return false;
812  }
813
814  private ConceptReferenceComponent findInInclude(Coding code) {
815    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
816      return null;
817    }
818    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
819      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
820        for (ConceptReferenceComponent cc : inc.getConcept()) {
821          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
822            return cc;
823          }
824        }
825      }
826    }
827    return null;
828  }
829
830  private CodeSystem findSpecialCodeSystem(String system, String version) {
831    if ("urn:ietf:rfc:3986".equals(system)) {
832      CodeSystem cs = new CodeSystem();
833      cs.setUrl(system);
834      cs.setUserData("tx.cs.special", new URICodeSystem());
835      cs.setContent(CodeSystemContentMode.COMPLETE);
836      return cs; 
837    }
838    return null;
839  }
840
841  private ValidationResult findCodeInExpansion(Coding code) {
842    if (valueset==null || !valueset.hasExpansion())
843      return null;
844    return findCodeInExpansion(code, valueset.getExpansion().getContains());
845  }
846
847  private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
848    for (ValueSetExpansionContainsComponent containsComponent: contains) {
849      opContext.deadCheck("findCodeInExpansion");
850      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
851        ConceptDefinitionComponent ccd = new ConceptDefinitionComponent();
852        ccd.setCode(containsComponent.getCode());
853        ccd.setDisplay(containsComponent.getDisplay());
854        ValidationResult res = new ValidationResult(code.getSystem(), code.hasVersion() ? code.getVersion() : containsComponent.getVersion(), ccd, getPreferredDisplay(ccd, null));
855        return res;
856      }
857      if (containsComponent.hasContains()) {
858        ValidationResult res = findCodeInExpansion(code, containsComponent.getContains());
859        if (res != null) {
860          return res;
861        }
862      }
863    }
864    return null;
865  }
866
867  private boolean checkExpansion(Coding code, VersionInfo vi) {
868    if (valueset==null || !valueset.hasExpansion()) {
869      return false;
870    }
871    return checkExpansion(code, valueset.getExpansion().getContains(), vi);
872  }
873
874  private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains, VersionInfo vi) {
875    for (ValueSetExpansionContainsComponent containsComponent: contains) {
876      opContext.deadCheck("checkExpansion: "+code.toString());
877      if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
878        vi.setExpansionVersion(containsComponent.getVersion());
879        return true;
880      }
881      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains(), vi)) {
882        return true;
883      }
884    }
885    return false;
886  }
887
888  private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) {
889    ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), cs.getCaseSensitive(), allAltCodes);
890    if (cc == null) {
891      cc = findSpecialConcept(code, cs);
892    }
893    if (cc == null) {
894      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
895        String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_FRAGMENT, code.getCode(), cs.getUrl(),  cs.getVersion());
896        return new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.InvalidCode, null));        
897      } else {
898        String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_VERSION, code.getCode(), cs.getUrl(), cs.getVersion());
899        return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.InvalidCode, null));
900      }
901    } else {
902      if (!cc.getCode().equals(code.getCode())) {
903        String msg = context.formatMessage(I18nConstants.CODE_CASE_DIFFERENCE, code.getCode(), cc.getCode(), cs.getVersionedUrl());
904        info.addIssue(makeIssue(IssueSeverity.INFORMATION, IssueType.BUSINESSRULE, path+".code", msg, OpIssueCode.CodeRule, null));
905      }
906    }
907    Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs));
908    if (vcc != null) {
909      vcc.addCoding(vc);
910    }
911
912    boolean inactive = (CodeSystemUtilities.isInactive(cs, cc));
913    String status = inactive ? (CodeSystemUtilities.getStatus(cs, cc)) : null;
914
915    boolean isDefaultLang = false;
916    boolean ws = false;     
917    if (code.getDisplay() == null) {
918      return new ValidationResult(code.getSystem(), cs.getVersion(), cc, vc.getDisplay()).setStatus(inactive, status);
919    }
920    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ", " or ");
921    if (cc.hasDisplay() && isOkLanguage(cs.getLanguage())) {
922      b.append("'"+cc.getDisplay()+"'"+(cs.hasLanguage() ? " ("+cs.getLanguage()+")" : ""));
923      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
924        return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
925      } else if (Utilities.normalize(code.getDisplay()).equals(Utilities.normalize(cc.getDisplay()))) {
926        ws = true;
927      }
928    } else if (cc.hasDisplay() && code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
929      isDefaultLang = true;
930    }
931    
932    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
933      opContext.deadCheck("validateCode1 "+ds.toString());
934      if (isOkLanguage(ds.getLanguage())) {
935        b.append("'"+ds.getValue()+"' ("+ds.getLanguage()+")");
936        if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
937          return new ValidationResult(code.getSystem(),cs.getVersion(),  cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
938        }
939        if (Utilities.normalize(code.getDisplay()).equalsIgnoreCase(Utilities.normalize(ds.getValue()))) {
940          ws = true;
941        }
942      }
943    }
944    // also check to see if the value set has another display
945    if (options.isUseValueSetDisplays()) {
946      ConceptReferencePair vs = findValueSetRef(code.getSystem(), code.getCode());
947      if (vs != null && (vs.getCc().hasDisplay() ||vs.getCc().hasDesignation())) {
948        if (vs.getCc().hasDisplay() && isOkLanguage(vs.getValueset().getLanguage())) {
949          b.append("'"+vs.getCc().getDisplay()+"'");
950          if (code.getDisplay().equalsIgnoreCase(vs.getCc().getDisplay())) {
951            return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
952          }
953        }
954        for (ConceptReferenceDesignationComponent ds : vs.getCc().getDesignation()) {
955          opContext.deadCheck("validateCode2 "+ds.toString());
956          if (isOkLanguage(ds.getLanguage())) {
957            b.append("'"+ds.getValue()+"'");
958            if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
959              return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
960            }
961          }
962        }
963      }
964    }
965    if (b.count() > 0) {
966      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());
967      return new ValidationResult(dispWarningStatus(), msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(dispWarning(), IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null)).setStatus(inactive, status);
968    } else if (isDefaultLang) {
969      // we didn't find any valid displays because there aren't any, so the default language is acceptable, but we'll still add a hint about that
970      boolean none = options.getLanguages().getLangs().size() == 1 && !hasLanguage(cs, options.getLanguages().getLangs().get(0));
971      String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), none ? I18nConstants.NO_VALID_DISPLAY_FOUND_LANG_NONE : I18nConstants.NO_VALID_DISPLAY_FOUND_LANG_SOME, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary(), code.getDisplay());
972      String n = null;
973      return new ValidationResult(IssueSeverity.INFORMATION, n, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(IssueSeverity.INFORMATION, IssueType.INVALID, path+".display", msg, OpIssueCode.DisplayComment, null)).setStatus(inactive, status);      
974    } else {
975      String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary());
976      return new ValidationResult(IssueSeverity.WARNING, msg, code.getSystem(), cs.getVersion(), cc, cc.getDisplay(), makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null)).setStatus(inactive, status);      
977    }
978  }
979
980  private boolean hasLanguage(CodeSystem cs, LanguagePreference languagePreference) {
981    String lang = languagePreference.getLang();
982    if (lang == null) {
983      return false;
984    }
985    for (ConceptDefinitionComponent cc : cs.getConcept()) {
986      boolean hl = hasLanguage(cs, cc, lang);
987      if (hl) {
988        return true;
989      }
990    }
991    return false;
992  }
993
994  private boolean hasLanguage(CodeSystem cs, ConceptDefinitionComponent cc, String lang) {
995    if (lang.equals(cs.getLanguage()) && cc.hasDisplay()) {
996      return true;
997    }
998    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
999      if (lang.equals(d.getLanguage())) {
1000        return true;
1001      }
1002    }
1003    for (ConceptDefinitionComponent cc1 : cc.getConcept()) {
1004      boolean hl = hasLanguage(cs, cc1, lang);
1005      if (hl) {
1006        return true;
1007      }
1008    }
1009    return false;
1010  }
1011
1012  private ConceptDefinitionComponent findSpecialConcept(Coding c, CodeSystem cs) {
1013    // handling weird special cases in v2 code systems 
1014    if ("http://terminology.hl7.org/CodeSystem/v2-0203".equals(cs.getUrl())) {
1015      String code = c.getCode();
1016      if (code != null && code.startsWith("NN") && code.length() > 3) {
1017        ConceptDefinitionComponent cd = findCountryCode(code.substring(2));
1018        if (cd != null) {
1019          return new ConceptDefinitionComponent(code).setDisplay("National Identifier for "+cd.getDisplay());
1020        }
1021      }      
1022    }
1023//    0396: HL7nnnn, IBTnnnn, ISOnnnn, X12Dennnn, 99zzz
1024//    0335: PRNxxx
1025    return null;
1026  }
1027
1028  
1029  private ConceptDefinitionComponent findCountryCode(String code) {
1030    ValidationResult vr = context.validateCode(new ValidationOptions(FhirPublication.R5), "urn:iso:std:iso:3166", null, code, null);
1031    return vr == null || !vr.isOk() ? null : new ConceptDefinitionComponent(code).setDisplay(vr.getDisplay()).setDefinition(vr.getDefinition());
1032  }
1033
1034  private IssueSeverity dispWarning() {
1035    return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.ERROR; 
1036  }
1037  
1038  private IssueSeverity dispWarningStatus() {
1039    return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; // information -> error later
1040  }
1041
1042  private boolean isOkLanguage(String language) {
1043    if (!options.hasLanguages()) {
1044      return true;
1045    }
1046    if (LanguageUtils.langsMatch(options.getLanguages(), language)) {
1047      return true;
1048    }
1049    if (language == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US") || options.isEnglishOk())) {
1050      return true;
1051    }
1052    return false;
1053  }
1054
1055  private ConceptReferencePair findValueSetRef(String system, String code) {
1056    if (valueset == null)
1057      return null;
1058    // if it has an expansion
1059    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
1060      opContext.deadCheck("findValueSetRef "+exp.toString());
1061      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
1062        ConceptReferenceComponent cc = new ConceptReferenceComponent();
1063        cc.setDisplay(exp.getDisplay());
1064        cc.setDesignation(exp.getDesignation());
1065        return new ConceptReferencePair(valueset, cc);
1066      }
1067    }
1068    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
1069      if (system.equals(inc.getSystem())) {
1070        for (ConceptReferenceComponent cc : inc.getConcept()) {
1071          if (cc.getCode().equals(code)) {
1072            return new ConceptReferencePair(valueset, cc);
1073          }
1074        }
1075      }
1076      for (CanonicalType url : inc.getValueSet()) {
1077        ConceptReferencePair cc = getVs(url.asStringValue(), null).findValueSetRef(system, code);
1078        if (cc != null) {
1079          return cc;
1080        }
1081      }
1082    }
1083    return null;
1084  }
1085
1086  /*
1087   * Check that all system values within an expansion correspond to the specified system value
1088   */
1089  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
1090    for (ValueSetExpansionContainsComponent contains : containsList) {
1091      if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) {
1092        return false;
1093      }
1094    }
1095    return true;
1096  }
1097
1098  private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) {
1099    opContext.deadCheck("findCodeInConcept: "+code.toString()+", "+concept.toString());
1100    if (code.equals(concept.getCode())) {
1101      return concept;
1102    }
1103    ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, caseSensitive, altCodeRules);
1104    if (cc != null) {
1105      return cc;
1106    }
1107    if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
1108      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
1109      for (ConceptDefinitionComponent c : children) {
1110        cc = findCodeInConcept(c, code, caseSensitive, altCodeRules);
1111        if (cc != null) {
1112          return cc;
1113        }
1114      }
1115    }
1116    return null;
1117  }
1118  
1119  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) {
1120    for (ConceptDefinitionComponent cc : concept) {
1121      if (code.equals(cc.getCode()) || (!caseSensitive && (code.equalsIgnoreCase(cc.getCode())))) {
1122        return cc;
1123      }
1124      if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
1125        return cc;
1126      }
1127      ConceptDefinitionComponent c = findCodeInConcept(cc, code, caseSensitive, altCodeRules);
1128      if (c != null) {
1129        return c;
1130      }
1131    }
1132    return null;
1133  }
1134
1135
1136  private List<String> alternateCodes(ConceptDefinitionComponent focus, AlternateCodesProcessingRules altCodeRules) {
1137    List<String> codes = new ArrayList<>();
1138    for (ConceptPropertyComponent p : focus.getProperty()) {
1139      if ("alternateCode".equals(p.getCode()) && (altCodeRules.passes(p.getExtension())) && p.getValue().isPrimitive()) {
1140        codes.add(p.getValue().primitiveValue());        
1141      }
1142    }
1143    return codes;
1144  }
1145
1146  
1147  private String systemForCodeInValueSet(String code, List<StringWithCode> problems) {
1148    Set<String> sys = new HashSet<>();
1149    if (!scanForCodeInValueSet(code, sys, problems)) {
1150      return null;
1151    }
1152    if (sys.size() == 0) {
1153      problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_INFER_CODESYSTEM, code, valueset.getVersionedUrl())));
1154      return null;
1155    } else if (sys.size() > 1) {
1156      problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, code, valueset.getVersionedUrl(), sys.toString())));
1157      return null;
1158    } else {
1159      return sys.iterator().next();
1160    }
1161  }
1162  
1163  private boolean scanForCodeInValueSet(String code, Set<String> sys, List<StringWithCode> problems) {
1164    if (valueset.hasCompose()) {
1165      //  ignore excludes - they can't make any difference
1166      if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) {
1167        problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, code, valueset.getVersionedUrl())));
1168      }
1169
1170      int i = 0;
1171      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
1172        opContext.deadCheck("scanForCodeInValueSet: "+code.toString());
1173        if (scanForCodeInValueSetInclude(code, sys, problems, i, vsi)) {
1174          return true;
1175        }
1176        i++;
1177      }
1178    } else if (valueset.hasExpansion()) {
1179      // Retrieve a list of all systems associated with this code in the expansion
1180      if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) {
1181        return false;
1182      }
1183    }
1184    return true;
1185  }
1186
1187  private boolean scanForCodeInValueSetInclude(String code, Set<String> sys, List<StringWithCode> problems, int i, ConceptSetComponent vsi) {
1188    if (vsi.hasValueSet()) {
1189      for (CanonicalType u : vsi.getValueSet()) {
1190        if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) {
1191          return false;
1192        }
1193      }
1194    } else if (!vsi.hasSystem()) { 
1195      problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, code, valueset.getVersionedUrl(), i)));
1196      return false;
1197    }
1198    if (vsi.hasSystem()) {
1199      if (vsi.hasFilter()) {
1200        ValueSet vsDummy = new ValueSet();
1201        vsDummy.setUrl(Utilities.makeUuidUrn());
1202        vsDummy.setStatus(PublicationStatus.ACTIVE);
1203        vsDummy.getCompose().addInclude(vsi);
1204        Coding c = new Coding().setCode(code).setSystem(vsi.getSystem());
1205        ValidationResult vr = context.validateCode(options.withGuessSystem(false), c, vsDummy);
1206        if (vr.isOk()) {
1207          sys.add(vsi.getSystem());
1208        } else {
1209          // problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_FILTER, code, valueset.getVersionedUrl(), i, vsi.getSystem(), filterSummary(vsi))));
1210          return false;
1211        }
1212      }
1213      CodeSystemProvider csp = CodeSystemProvider.factory(vsi.getSystem());
1214      if (csp != null) {
1215        Boolean ok = csp.checkCode(code);
1216        if (ok == null) {
1217          problems.add(new StringWithCode(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE, code, valueset.getVersionedUrl(), vsi.getSystem())));
1218          sys.add(vsi.getSystem());
1219        } else if (ok) {
1220          sys.add(vsi.getSystem());
1221        }
1222      } else {
1223        CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion());
1224        if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) {
1225
1226          if (vsi.hasConcept()) {
1227            for (ConceptReferenceComponent cc : vsi.getConcept()) {
1228              boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
1229              if (match) {
1230                sys.add(vsi.getSystem());
1231              }
1232            }
1233          } else {
1234            ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, cs.getCaseSensitive(), allAltCodes);
1235            if (cc != null) {
1236              sys.add(vsi.getSystem());
1237            }
1238          }
1239        } else if (vsi.hasConcept()) {
1240          for (ConceptReferenceComponent cc : vsi.getConcept()) {
1241            boolean match = cc.getCode().equals(code);
1242            if (match) {
1243              sys.add(vsi.getSystem());
1244            }
1245          }
1246        } else {
1247          ValueSet vsDummy = new ValueSet();
1248          vsDummy.setUrl(Utilities.makeUuidUrn());
1249          vsDummy.setStatus(PublicationStatus.ACTIVE);
1250          vsDummy.getCompose().addInclude(vsi);
1251          ValidationResult vr = context.validateCode(options.withNoClient(), code, vsDummy);
1252          if (vr.isOk()) {
1253            sys.add(vsi.getSystem());
1254          } else {
1255            // ok, we'll try to expand this one then 
1256            ValueSetExpansionOutcome vse = context.expandVS(vsi, false, false);
1257            if (vse.isOk()) {
1258              if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) {
1259                return false;
1260              }
1261            } else {
1262              problems.add(new StringWithCode(OpIssueCode.NotFound, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM, code, valueset.getVersionedUrl(), i, vsi.getSystem(), vse.getAllErrors().toString())));              
1263              return false;
1264            }
1265
1266          }
1267        }
1268      }
1269    }
1270    return false;
1271  }
1272
1273  private String filterSummary(ConceptSetComponent vsi) {
1274    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1275    for (ConceptSetFilterComponent f : vsi.getFilter()) {
1276      b.append(f.getProperty()+f.getOp().toCode()+f.getValue());
1277    }
1278    return b.toString();
1279  }
1280
1281  private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<StringWithCode> problems) {
1282    ValueSetValidator vs = getVs(uri, null);
1283    return vs.scanForCodeInValueSet(code, sys, problems);
1284  }
1285
1286  /*
1287   * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding
1288   * to the passed list. 
1289   */
1290  private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<StringWithCode> problems) {
1291    for (ValueSetExpansionContainsComponent c: contains) {
1292      opContext.deadCheck("checkSystems "+code.toString());
1293      if (c.getCode().equals(code)) {
1294        systems.add(c.getSystem());
1295      }
1296      if (c.hasContains())
1297        checkSystems(c.getContains(), code, systems, problems);
1298    }
1299    return true;
1300  }
1301  
1302  public Boolean codeInValueSet(String path, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
1303    if (valueset == null) {
1304      return null;
1305    }
1306    opContext.deadCheck("codeInValueSet: "+system+"#"+code);
1307    checkCanonical(info.getIssues(), path, valueset, valueset);
1308    Boolean result = false;
1309    VersionInfo vi = new VersionInfo(this);
1310      
1311    if (valueset.hasExpansion()) {
1312      return checkExpansion(new Coding(system, code, null), vi);
1313    } else if (valueset.hasCompose()) {
1314      int i = 0;
1315      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
1316        Boolean ok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info);
1317        i++;
1318        if (ok == null && result != null && result == false) {
1319          result = null;
1320        } else if (ok != null && ok) {
1321          result = true;
1322          break;
1323        }
1324      }
1325      i = valueset.getCompose().getInclude().size();
1326      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
1327        Boolean nok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info);
1328        i++;
1329        if (nok == null && result != null && result == false) {
1330          result = null;
1331        } else if (nok != null && nok) {
1332          result = false;
1333        }
1334      }
1335    } 
1336
1337    return result;
1338  }
1339
1340  private Boolean inComponent(String path, ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException {
1341    opContext.deadCheck("inComponent "+vsiIndex);
1342    boolean ok = true;
1343    
1344    if (vsi.hasValueSet()) {
1345      if (isValueSetUnionImports()) {
1346        ok = false;
1347        for (UriType uri : vsi.getValueSet()) {
1348          if (inImport(path, uri.getValue(), system, version, code, info)) {
1349            return true;
1350          }
1351        }
1352      } else {
1353        Boolean bok = inImport(path, vsi.getValueSet().get(0).getValue(), system, version, code, info);
1354        if (bok == null) {
1355          return bok;
1356        }
1357        ok = bok;
1358        for (int i = 1; i < vsi.getValueSet().size(); i++) {
1359          UriType uri = vsi.getValueSet().get(i);
1360          ok = ok && inImport(path, uri.getValue(), system, version, code, info); 
1361        }
1362      }
1363    }
1364
1365    if (!vsi.hasSystem() || !ok) {
1366      return ok;
1367    }
1368    
1369    if (only && system == null) {
1370      // whether we know the system or not, we'll accept the stated codes at face value
1371      for (ConceptReferenceComponent cc : vsi.getConcept()) {
1372        if (cc.getCode().equals(code)) {
1373          return true;
1374        }
1375      }
1376    }
1377
1378    if (system == null || !system.equals(vsi.getSystem()))
1379      return false;
1380    // ok, we need the code system
1381    CodeSystem cs = resolveCodeSystem(system, version);
1382    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
1383      if (throwToServer) {
1384        // make up a transient value set with
1385        ValueSet vs = new ValueSet();
1386        vs.setStatus(PublicationStatus.ACTIVE);
1387        vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
1388        vs.setVersion(valueset.getVersion());
1389        vs.getCompose().addInclude(vsi);
1390        opContext.deadCheck("hit server "+vs.getVersionedUrl());
1391        ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs);
1392        if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
1393          if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
1394            // server didn't know the code system either - we'll take it face value
1395            if (!info.hasNotFound(system)) {
1396              String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system);
1397              info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, msg, OpIssueCode.NotFound, null));
1398              for (ConceptReferenceComponent cc : vsi.getConcept()) {
1399                if (cc.getCode().equals(code)) {
1400                  opContext.deadCheck("server true");
1401                  return true;
1402                }
1403              }
1404            }
1405            info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
1406            opContext.deadCheck("server codesystem unsupported");
1407            return null;
1408          }
1409          opContext.deadCheck("server not found");
1410          return false;
1411        }
1412        if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
1413          opContext.deadCheck("server no server");
1414          throw new NoTerminologyServiceException();
1415        }
1416        return res.isOk();
1417      } else {
1418        info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
1419        if (unknownSystems != null) {
1420          if (version == null) {
1421            unknownSystems.add(system);
1422          } else {
1423            unknownSystems.add(system+"|"+version);          
1424          }
1425        }
1426        return null;
1427      }
1428    } else {
1429      checkCanonical(info.getIssues(), path, cs, valueset);
1430      if ((valueset.getCompose().hasInactive() && !valueset.getCompose().getInactive()) || options.isActiveOnly()) {
1431        if (CodeSystemUtilities.isInactive(cs, code)) {
1432          info.addIssue(makeIssue(IssueSeverity.ERROR, IssueType.BUSINESSRULE, path+".code", context.formatMessage(I18nConstants.STATUS_CODE_WARNING_CODE, "not active", code), OpIssueCode.CodeRule, null));        
1433          return false;
1434        }
1435      }
1436      
1437      if (vsi.hasFilter()) {
1438        ok = true;
1439        for (ConceptSetFilterComponent f : vsi.getFilter()) {
1440          if (!codeInFilter(cs, system, f, code)) {
1441            return false;
1442          }
1443        }
1444      }
1445
1446      List<ConceptDefinitionComponent> list = cs.getConcept();
1447      ok = validateCodeInConceptList(code, cs, list, allAltCodes);
1448      if (ok && vsi.hasConcept()) {
1449        for (ConceptReferenceComponent cc : vsi.getConcept()) {
1450          if (cc.getCode().equals(code)) { 
1451            return true;
1452          }
1453        }
1454        return false;
1455      } else {
1456        // recheck that this is a valid alternate code
1457        ok = validateCodeInConceptList(code, cs, list, altCodeParams);
1458        return ok;
1459      }
1460    }
1461  }
1462
1463  protected boolean isValueSetUnionImports() {
1464    PackageInformation p = (PackageInformation) valueset.getSourcePackage();
1465    if (p != null) {
1466      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
1467    } else {
1468      return false;
1469    }
1470  }
1471
1472  private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException {
1473    if ("concept".equals(f.getProperty()))
1474      return codeInConceptFilter(cs, f, code);
1475    else if ("code".equals(f.getProperty()) && f.getOp() == FilterOperator.REGEX)
1476      return codeInRegexFilter(cs, f, code);
1477    else if (CodeSystemUtilities.isDefinedProperty(cs, f.getProperty())) {
1478      return codeInPropertyFilter(cs, f, code);
1479    } else if (isKnownProperty(f.getProperty())) {
1480      return codeInKnownPropertyFilter(cs, f, code);
1481    } else {
1482      System.out.println("todo: handle filters with property = "+f.getProperty()+" "+f.getOp().toCode()); 
1483      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty(), f.getOp().toCode()));
1484    }
1485  }
1486
1487  private boolean isKnownProperty(String code) {
1488    return Utilities.existsInList(code, "notSelectable");
1489  }
1490
1491  private boolean codeInPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1492    switch (f.getOp()) {
1493    case EQUAL:
1494      if (f.getValue() == null) {
1495        return false;
1496      }
1497      DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1498      return d != null && f.getValue().equals(d.primitiveValue());
1499    case EXISTS: 
1500      return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null;
1501    case REGEX:
1502      if (f.getValue() == null) {
1503        return false;
1504      }
1505      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1506      return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue());
1507    case IN:
1508      if (f.getValue() == null) {
1509        return false;
1510      }
1511      String[] values = f.getValue().split("\\,");
1512      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1513      if (d != null) {
1514        String v = d.primitiveValue();
1515        for (String value : values) {
1516          if (v != null && v.equals(value.trim())) {
1517            return true;
1518          }
1519        }
1520      }
1521      return false;
1522    case NOTIN:
1523      if (f.getValue() == null) {
1524        return true;
1525      }
1526      values = f.getValue().split("\\,");
1527      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1528      if (d != null) {
1529        String v = d.primitiveValue();
1530        for (String value : values) {
1531          if (v != null && v.equals(value.trim())) {
1532            return false;
1533          }
1534        }
1535      }
1536      return true;
1537    default:
1538      System.out.println("todo: handle property filters with op = "+f.getOp()); 
1539      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1540    }
1541  }
1542  
1543  private boolean codeInKnownPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1544
1545    switch (f.getOp()) {
1546    case EQUAL:
1547      if (f.getValue() == null) {
1548        return false;
1549      }
1550      DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1551      return d != null && f.getValue().equals(d.primitiveValue());
1552    case EXISTS: 
1553      return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null;
1554    case REGEX:
1555      if (f.getValue() == null) {
1556        return false;
1557      }
1558      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1559      return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue());
1560    default:
1561      System.out.println("todo: handle known property filters with op = "+f.getOp()); 
1562      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1563    }
1564  }
1565
1566  private boolean codeInRegexFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1567    return code.matches(f.getValue());
1568  }
1569
1570  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
1571    switch (f.getOp()) {
1572    case ISA: return codeInConceptIsAFilter(cs, f, code, false);
1573    case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false);
1574    case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 
1575    default:
1576      System.out.println("todo: handle concept filters with op = "+f.getOp()); 
1577      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1578    }
1579  }
1580
1581  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean excludeRoot) {
1582    if (!excludeRoot && code.equals(f.getValue())) {
1583      return true;
1584    }
1585    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), cs.getCaseSensitive(), altCodeParams);
1586    if (cc == null) {
1587      return false;
1588    }
1589    ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, cs.getCaseSensitive(), altCodeParams);
1590    return cc2 != null && cc2 != cc;
1591  }
1592
1593  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list, AlternateCodesProcessingRules altCodeRules) {
1594    opContext.deadCheck("validateCodeInConceptList");
1595    if (def.hasUserData("tx.cs.special")) {
1596      return ((SpecialCodeSystem) def.getUserData("tx.cs.special")).findConcept(new Coding().setCode(code)) != null; 
1597    } else if (def.getCaseSensitive()) {
1598      for (ConceptDefinitionComponent cc : list) {
1599        if (cc.getCode().equals(code)) { 
1600          return true;
1601        }
1602        if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
1603          return true;
1604        }
1605        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) {
1606          return true;
1607        }
1608      }
1609    } else {
1610      for (ConceptDefinitionComponent cc : list) {
1611        if (cc.getCode().equalsIgnoreCase(code)) { 
1612          return true;
1613        }
1614        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) {
1615          return true;
1616        }
1617      }
1618    }
1619    return false;
1620  }
1621
1622  private ValueSetValidator getVs(String url, ValidationProcessInfo info) {
1623    if (inner.containsKey(url)) {
1624      return inner.get(url);
1625    }
1626    ValueSet vs = context.findTxResource(ValueSet.class, url, valueset);
1627    if (vs == null && info != null) {
1628      unknownValueSets.add(url);
1629      info.addIssue(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, null, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_VALUE_SET_, url), OpIssueCode.NotFound, null));
1630    }
1631    ValueSetValidator vsc = new ValueSetValidator(context, opContext.copy(), options, vs, localContext, expansionProfile, tcm);
1632    vsc.setThrowToServer(throwToServer);
1633    inner.put(url, vsc);
1634    return vsc;
1635  }
1636  
1637  private Boolean inImport(String path, String uri, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
1638    ValueSetValidator vs = getVs(uri, info);
1639    if (vs == null) {
1640      return false;
1641    } else {
1642      Boolean ok = vs.codeInValueSet(path, system, version, code, info);
1643      return ok;
1644    }
1645  }
1646
1647
1648  private String getPreferredDisplay(ConceptReferenceComponent cc) {
1649    if (!options.hasLanguages()) {
1650      return cc.getDisplay();
1651    }
1652    if (LanguageUtils.langsMatch(options.getLanguages(), valueset.getLanguage())) {
1653      return cc.getDisplay();
1654    }
1655    // if there's no language, we default to accepting the displays as (US) English
1656    if (valueset.getLanguage() == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
1657      return cc.getDisplay();
1658    }
1659    for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
1660      if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1661        return d.getValue();
1662      }
1663    }
1664    for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
1665      if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1666        return d.getValue();
1667      }
1668    }
1669    return cc.getDisplay();
1670  }
1671
1672
1673  private String getPreferredDisplay(ConceptDefinitionComponent cc, CodeSystem cs) {
1674    if (!options.hasLanguages()) {
1675      return cc.getDisplay();
1676    }
1677    if (cs != null && LanguageUtils.langsMatch(options.getLanguages(), cs.getLanguage())) {
1678      return cc.getDisplay();
1679    }
1680    // if there's no language, we default to accepting the displays as (US) English
1681    if ((cs == null || cs.getLanguage() == null) && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
1682      return cc.getDisplay();
1683    }
1684    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
1685      if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1686        return d.getValue();
1687      }
1688    }
1689    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
1690      if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
1691        return d.getValue();
1692      }
1693    }
1694    return cc.getDisplay();
1695  }
1696
1697}