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