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