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.*;
035
036
037import lombok.Getter;
038import lombok.Setter;
039import lombok.extern.slf4j.Slf4j;
040import org.hl7.fhir.exceptions.FHIRException;
041import org.hl7.fhir.exceptions.NoTerminologyServiceException;
042import org.hl7.fhir.r5.context.BaseWorkerContext;
043import org.hl7.fhir.r5.context.ContextUtilities;
044import org.hl7.fhir.r5.context.IWorkerContext;
045import org.hl7.fhir.r5.elementmodel.LanguageUtils;
046import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
047import org.hl7.fhir.r5.fhirpath.TypeDetails;
048import org.hl7.fhir.r5.model.*;
049import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode;
050import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
051import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
052import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
053import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
054import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
055import org.hl7.fhir.r5.model.OperationOutcome.IssueType;
056import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent;
057import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
058import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
059import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
060import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
061import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
062import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent;
063import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
064import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager;
065import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
066import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider;
067import org.hl7.fhir.r5.terminologies.providers.SpecialCodeSystem;
068import org.hl7.fhir.r5.terminologies.providers.URICodeSystem;
069import org.hl7.fhir.r5.terminologies.utilities.*;
070import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException;
071import org.hl7.fhir.r5.utils.OperationOutcomeUtilities;
072
073import org.hl7.fhir.r5.utils.UserDataNames;
074import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
075import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
076import org.hl7.fhir.utilities.*;
077import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
078import org.hl7.fhir.utilities.i18n.subtag.LanguageSubtagRegistry;
079import org.hl7.fhir.utilities.i18n.I18nConstants;
080import org.hl7.fhir.utilities.i18n.LanguageTag;
081import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
082import org.hl7.fhir.utilities.validation.ValidationOptions;
083
084import javax.annotation.Nonnull;
085
086@MarkedToMoveToAdjunctPackage
087@Slf4j
088public class ValueSetValidator extends ValueSetProcessBase {
089
090  public static final String NO_TRY_THE_SERVER = "The local terminology server cannot handle this request";
091  private TerminologyServiceErrorClass unknownSystemError;
092
093  public class StringWithCodes {
094    private OpIssueCode code;
095    private String message;
096    private String messageId;
097    protected StringWithCodes(OpIssueCode code, String message, String messageId) {
098      super();
099      this.code = code;
100      this.message = message;
101      this.messageId = messageId;
102    }
103    public OpIssueCode getCode() {
104      return code;
105    }
106    public String getMessage() {
107      return message;
108    }
109    public String getMessageId() {return messageId;}
110  }
111
112  private ValueSet valueset;
113  private Map<String, ValueSetValidator> inner = new HashMap<>();
114  private ValidationOptions options;
115  private ValidationContextCarrier localContext;
116  private List<CodeSystem> localSystems = new ArrayList<>();
117  protected Parameters expansionParameters;
118  private TerminologyClientManager tcm;
119  @Getter
120  @Setter
121  private Set<String> unknownSystems;
122  private Set<String> unknownValueSets = new HashSet<>();
123  @Setter
124  @Getter
125  private boolean throwToServer;
126  private LanguageSubtagRegistry registry;
127  private Set<String> checkedVersionCombinations = new HashSet<>();
128
129
130  public ValueSetValidator(BaseWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, Parameters expansionProfile, TerminologyClientManager tcm, LanguageSubtagRegistry registry) {
131    super(context, opContext);
132    this.valueset = source;
133    this.options = options;
134    this.expansionParameters = expansionProfile;
135    this.tcm = tcm;
136    this.registry = registry;
137    analyseValueSet();
138  }
139  
140  public ValueSetValidator(BaseWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyClientManager tcm, LanguageSubtagRegistry registry) {
141    super(context, opContext);
142    this.valueset = source;
143    this.options = options.copy();
144    this.options.setEnglishOk(true);
145    this.localContext = ctxt;
146    this.expansionParameters = expansionProfile;
147    this.tcm = tcm;
148    this.registry = registry;
149    analyseValueSet();
150  }
151
152  private void analyseValueSet() {
153    opContext.note("analyse");
154    if (valueset != null) {
155      opContext.note("vs = "+valueset.getVersionedUrl());
156      opContext.seeContext(valueset.getVersionedUrl());
157      for (Extension s : valueset.getExtensionsByUrl(ExtensionDefinitions.EXT_VS_CS_SUPPL_NEEDED)) {
158        requiredSupplements.add(s.getValue().primitiveValue());
159      }
160
161      if (!requiredSupplements.isEmpty()) {
162        for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
163          if (inc.hasSystem()) {
164            checkCodeSystemResolves(inc);
165          }
166        }
167        for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
168          if (inc.hasSystem()) {
169            checkCodeSystemResolves(inc);
170          }
171        }
172      }
173    } else {
174      opContext.note("vs = null");
175    }
176
177    altCodeParams.seeParameters(expansionParameters);
178    altCodeParams.seeValueSet(valueset);
179    if (localContext != null) {
180      if (valueset != null) {
181        for (ConceptSetComponent i : valueset.getCompose().getInclude()) {
182          analyseComponent(i, "inc"+i);
183        }
184        for (ConceptSetComponent i : valueset.getCompose().getExclude()) {
185          analyseComponent(i, "exc"+i);
186        }
187      }
188    }
189    opContext.note("analysed");
190  }
191
192  private void checkCodeSystemResolves(ConceptSetComponent c) {
193    CodeSystem csa = context.fetchCodeSystem(c.getSystem());
194    VersionAlgorithm va = csa == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(csa.getVersionAlgorithm());
195    String version = determineVersion(c.getSystem(), c.getVersion(), va);
196    CodeSystem cs = resolveCodeSystem(c.getSystem(), version, valueset);
197    if (cs == null) {
198      // well, it doesn't really matter at this point. Mainly we're triggering the supplement analysis to happen 
199      opContext.note("Unable to resolve "+c.getSystem()+"#"+version);
200    } else {
201      checkVersion(null, c.getSystem(), cs.getVersion(), va, null);
202    }
203  }
204
205  private void analyseComponent(ConceptSetComponent i, String name) {
206    opContext.deadCheck("analyse Component "+name);
207    if (i.getSystemElement().hasExtension(ExtensionDefinitions.EXT_VALUESET_SYSTEM)) {
208      String ref = i.getSystemElement().getExtensionString(ExtensionDefinitions.EXT_VALUESET_SYSTEM);
209      if (ref.startsWith("#")) {
210        String id = ref.substring(1);
211        for (ValidationContextResourceProxy t : localContext.getResources()) {
212          CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class);
213          if (cs != null) {
214            localSystems.add(cs);
215          }
216        }
217      } else {        
218        throw new Error("Not done yet #2: "+ref);
219      }
220    }    
221  }
222
223  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
224    return validateCode("CodeableConcept", code);
225  }
226  
227  public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException {
228    opContext.deadCheck("validate "+code.toString());
229    checkValueSetOptions();
230
231    // first, we validate the codings themselves
232    ValidationProcessInfo info = new ValidationProcessInfo();
233    
234    if (throwToServer) {
235      checkValueSetLoad(info);
236    }
237
238    CodeableConcept vcc = new CodeableConcept();
239    List<ValidationResult> resList = new ArrayList<>();
240    
241    if (!options.isMembershipOnly()) {
242      int i = 0;
243      for (Coding c : code.getCoding()) {
244        if (!c.hasSystem() && !c.hasUserData(UserDataNames.tx_val_sys_error)) {
245          c.setUserData(UserDataNames.tx_val_sys_error, true);
246          info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".coding["+i+"]",
247            context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), OpIssueCode.InvalidData, null, I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE));
248        } else {
249          checkExpansion(c);
250          checkInclude(c);
251          CodeSystem csa = context.fetchCodeSystem(c.getSystem());
252          VersionAlgorithm va = csa == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(csa.getVersionAlgorithm());
253          String version = determineVersion(c.getSystem(), c.getVersion(), va);
254          CodeSystem cs = resolveCodeSystem(c.getSystem(), version, valueset);
255          ValidationResult res = null;
256          if (cs != null) {
257            checkVersion(null, c.getSystem(), cs.getVersion(), va, null);
258          }
259          if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.SUPPLEMENT)) {
260            if (context.isNoTerminologyServer()) {
261              if (c.hasVersion()) {
262                String msg = getUnknownCodeSystemMessage(c.getSystem(), c.getVersion());
263                res = new ValidationResult(IssueSeverity.ERROR, msg,
264                  makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.NotFound, null, getUnknownCodeSystemMessageId(c.getSystem(), c.getVersion()))).setUnknownSystems(unknownSystems);
265              } else {
266                String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, c.getSystem(), c.getVersion());
267                unknownSystems.add(c.getSystem());
268                res = new ValidationResult(IssueSeverity.ERROR, msg,
269                  makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.NotFound, null, I18nConstants.UNKNOWN_CODESYSTEM)).setUnknownSystems(unknownSystems);
270              }
271            } else {
272              res = context.validateCode(options.withNoClient(), c, null);
273              if (res.isOk()) {
274                vcc.addCoding(new Coding().setCode(res.getCode()).setVersion(res.getVersion()).setSystem(res.getSystem()).setDisplay(res.getDisplay()));
275              }
276              for (OperationOutcomeIssueComponent iss : res.getIssues()) {
277                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")) {
278                  iss.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING);
279                  res.setSeverity(IssueSeverity.WARNING);
280                }
281                iss.resetPath("Coding", path+".coding["+i+"]");
282              }
283              if (res.isInactive()) {
284                String status = res.getStatus();
285                if (status == null) {
286                  status = "inactive";
287                }
288                String msg = context.formatMessage(I18nConstants.INACTIVE_CONCEPT_FOUND, status, c.getCode());
289                res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.BUSINESSRULE, path, msg, OpIssueCode.CodeComment, res.getServer(), I18nConstants.INACTIVE_CONCEPT_FOUND));
290                res.mineIssues(res.getIssues());
291              }
292            }
293          } else if (cs.getContent() == CodeSystemContentMode.SUPPLEMENT || cs.hasSupplements()) {
294            String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getVersionedUrl());
295            res = new ValidationResult(IssueSeverity.ERROR, msg,
296              makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, path+".coding["+i+"].system", msg, OpIssueCode.InvalidData, null, I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT));
297          } else {
298            c.setUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM, cs);
299
300            checkCanonical(info.getIssues(), path, cs, valueset);
301            res = validateCode(path+".coding["+i+"]", c, cs, vcc, info);
302          }
303          info.getIssues().addAll(res.getIssues());
304          if (res != null) {
305            resList.add(res);
306            if (!res.isOk() && !res.messageIsInIssues()) {
307              // no message ids here
308              if (res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
309                info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, path+".coding["+i+"]", res.getMessage(), OpIssueCode.NotFound, res.getServer(), null));
310              } else {
311                info.getIssues().addAll(makeIssue(res.getSeverity(), IssueType.CODEINVALID, path+".coding["+i+"]", res.getMessage(), OpIssueCode.InvalidCode, res.getServer(), null));
312              }
313            }
314          } 
315        }
316        i++;
317      }
318    }
319    Coding foundCoding = null;
320    String msg = null;
321    Boolean result = false;
322    if (valueset != null) {
323      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ");
324      List<String> cpath = new ArrayList<>();
325      
326      int i = 0;
327      for (Coding c : code.getCoding()) {
328        String cs = "'"+c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode()+(c.hasDisplay() ? " ('"+c.getDisplay()+"')" : "")+"'";
329        String cs2 = c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "");
330        cpath.add(path+".coding["+i+"]");
331        b.append(cs2);
332        Boolean ok = codeInValueSet(path+".coding["+i+"]", c.getSystem(), c.getVersion(), c.getCode(), info);
333        if (ok == null && result != null && result == false) {
334          result = null;
335        } else if (ok != null && ok) {
336          result = true;
337          foundCoding = c.copy();
338          foundCoding.setVersion(info.getFoundVersion());
339          if (!options.isMembershipOnly()) {
340            vcc.addCoding().setSystem(c.getSystem()).setVersion(info.getFoundVersion()).setCode(c.getCode());
341          }
342        }
343        if (ok == null || !ok) {
344          vcc.removeCoding(c.getSystem(), c.getVersion(), c.getCode());          
345        }
346        if (ok != null && !ok) {
347          msg = context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE, null, valueset.getVersionedUrl(), cs);
348          info.getIssues().addAll(makeIssue(IssueSeverity.INFORMATION, IssueType.CODEINVALID, path+".coding["+i+"].code", msg, OpIssueCode.ThisNotInVS, null, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE));
349        }
350        i++;
351      }
352      if (result == null) {
353//        String msgid = null;
354//        if (!unknownValueSets.isEmpty()) {
355//          msgid = I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS;
356//          msg = context.formatMessage(msgid, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(", ", unknownValueSets));
357//        } else {
358//          msgid = I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS;
359//          msg = context.formatMessage(msgid, valueset.getVersionedUrl(), b.toString());
360//        }
361//        info.getIssues().addAll(makeIssue(IssueSeverity.WARNING, unknownSystems.isEmpty() && unknownValueSets.isEmpty() ? IssueType.CODEINVALID : IssueType.NOTFOUND, null, msg, OpIssueCode.VSProcessing, null, msgid));
362      } else if (!result) {
363        // to match Ontoserver
364        OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR, org.hl7.fhir.r5.model.OperationOutcome.IssueType.CODEINVALID);
365        iss.getDetails().setText(context.formatMessage(I18nConstants.TX_GENERAL_CC_ERROR_MESSAGE, valueset.getVersionedUrl()));
366        iss.getDetails().addCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", OpIssueCode.NotInVS.toCode(), null);
367        iss.addExtension(ExtensionDefinitions.EXT_ISSUE_MSG_ID, new StringType(I18nConstants.TX_GENERAL_CC_ERROR_MESSAGE));
368        info.getIssues().add(iss);
369
370//        msg = context.formatMessagePlural(code.getCoding().size(), I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), b.toString());
371//        info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, code.getCoding().size() == 1 ? path+".coding[0].code" : path, msg));
372      }
373    }
374
375    if (vcc.hasCoding() && code.hasText()) {
376      vcc.setText(code.getText());
377    }
378    if (!checkRequiredSupplements(info)) {
379      return new ValidationResult(IssueSeverity.ERROR, info.getIssues().get(info.getIssues().size()-1).getDetails().getText(), info.getIssues());
380    } else if (info.hasErrors()) {
381      ValidationResult res = new ValidationResult(IssueSeverity.ERROR, null, info.getIssues());
382      if (foundCoding != null) {
383        ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
384        cd.setDisplay(lookupDisplay(foundCoding));
385        res.setDefinition(cd);
386        res.setSystem(foundCoding.getSystem());
387        res.setVersion(foundCoding.hasVersion() ? foundCoding.getVersion() : foundCoding.hasUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM) ? ((CodeSystem) foundCoding.getUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM)).getVersion() : null);
388        res.setDisplay(cd.getDisplay());
389      }
390      if (info.getErr() != null) {
391        res.setErrorClass(info.getErr());
392      } else if (unknownSystemError != null) {
393        res.setErrorClass(unknownSystemError);
394      }
395      res.setUnknownSystems(unknownSystems);
396      res.addCodeableConcept(vcc);
397      return res;
398    } else if (result == null) {
399      return new ValidationResult(IssueSeverity.WARNING, null, info.getIssues());
400    } else if (foundCoding == null && valueset != null) {
401      return new ValidationResult(IssueSeverity.ERROR, "Internal Error that should not happen",
402        makeIssue(IssueSeverity.FATAL, IssueType.EXCEPTION, path, "Internal Error that should not happen", OpIssueCode.VSProcessing, null, null));
403    } else if (info.getIssues().size() > 0) {
404      if (foundCoding == null) {
405        IssueSeverity lvl = IssueSeverity.INFORMATION; 
406        for (OperationOutcomeIssueComponent iss : info.getIssues()) {
407          lvl = IssueSeverity.max(lvl, OperationOutcomeUtilities.convert(iss.getSeverity()));
408        }
409        return new ValidationResult(lvl, null, info.getIssues());
410      } else {
411        String disp = lookupDisplay(foundCoding);
412        ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
413        cd.setDisplay(disp);
414        return new ValidationResult(IssueSeverity.WARNING, info.summaryList(), foundCoding.getSystem(), getVersion(foundCoding), cd, disp, info.getIssues()).addCodeableConcept(vcc);
415      }
416    } else if (!result) {
417      if (valueset != null) {
418        throw new Error("This should never happen: no result but is value set");
419      } else if (vcc.hasCoding()) {
420        return new ValidationResult(vcc.getCodingFirstRep().getSystem(), getVersion(vcc.getCodingFirstRep()), new ConceptDefinitionComponent(vcc.getCodingFirstRep().getCode()).setDisplay(vcc.getCodingFirstRep().getDisplay()), vcc.getCodingFirstRep().getDisplay()).addCodeableConcept(vcc);
421      } else {
422        throw new Error("This should never happen: no result, no value set, no coding");
423      }
424    } else if (foundCoding != null) {
425      ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
426      cd.setDisplay(lookupDisplay(foundCoding));
427      return new ValidationResult(foundCoding.getSystem(), getVersion(foundCoding), cd, getPreferredDisplay(cd, null)).addCodeableConcept(vcc);
428    } else {
429      throw new Error("This should never happen - ther response from the server could not be understood");
430    }
431  }
432
433  private String getUnknownCodeSystemMessage(String system, String version) {
434    Set<String> set = resolveCodeSystemVersions(system);
435    String msg;
436    if (set.isEmpty()) {
437      msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION_NONE, system, version);
438      unknownSystems.add(system);
439    } else {
440      msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, version, CommaSeparatedStringBuilder.join(",", Utilities.sorted(set)));
441      unknownSystemError = TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED;
442      unknownSystems.add(system + "|" + version);
443    }
444    return msg;
445  }
446
447  private String getUnknownCodeSystemMessageId(String system, String version) {
448    Set<String> set = resolveCodeSystemVersions(system);
449    if (set.isEmpty()) {
450      return I18nConstants.UNKNOWN_CODESYSTEM_VERSION_NONE;
451    } else {
452      return I18nConstants.UNKNOWN_CODESYSTEM_VERSION;
453    }
454  }
455
456  private void checkValueSetLoad(ValidationProcessInfo info) {
457    int serverCount = getServerLoad(info);
458    // There's a trade off here: if we're going to hit the server inside the components, then
459    // the amount of value set collateral we send is limited, but we pay the price of hitting 
460    // the server multiple times. If, on the other hand, we give up on that, and hit the server 
461    // directly, we have to send value set collateral (though we cache at the higher level)
462    //
463    // the cutoff value is chosen experimentally
464    if (serverCount > 2) {
465      throw new VSCheckerException("This value set is better processed on the server for performance reasons", null, true);
466    }
467  }
468
469  private int getServerLoad(ValidationProcessInfo info) {
470    int serverCount = 0;
471    if (valueset != null) {
472      for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
473        serverCount = serverCount + checkValueSetLoad(inc, info);
474      }
475      for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
476        serverCount = serverCount + checkValueSetLoad(inc, info);
477      }
478    }
479    return serverCount;
480  }
481  
482  private int checkValueSetLoad(ConceptSetComponent inc, ValidationProcessInfo info) {
483    int serverCount = 0;
484    for (UriType uri : inc.getValueSet()) {
485      String url = getCu().pinValueSet(uri.getValue(), expansionParameters);
486      ValueSetValidator vsv = getVs(url, info);
487      serverCount += vsv.getServerLoad(info);
488    }
489    if (inc.hasSystem()) {
490      CodeSystem cs = resolveCodeSystem(inc.getSystem(), inc.getVersion(), valueset);
491      if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
492        serverCount++;
493      }
494    }
495    return serverCount;
496  }
497
498  private boolean checkRequiredSupplements(ValidationProcessInfo info) {
499    if (!requiredSupplements.isEmpty()) {
500      String msg = context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements));
501      throw new TerminologyServiceProtectionException(msg, TerminologyServiceErrorClass.BUSINESS_RULE, IssueType.NOTFOUND);
502    }
503    return requiredSupplements.isEmpty();
504  }
505
506  private boolean valueSetDependsOn(String system, String version) {
507    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
508      if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) {
509        return true;
510      }
511    }
512    return false;
513  }
514
515  private String getVersion(Coding c) {
516    if (c.hasVersion()) {
517      return c.getVersion();
518    } else if (c.hasUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM)) {
519      return ((CodeSystem) c.getUserData(UserDataNames.TX_ASSOCIATED_CODESYSTEM)).getVersion();
520    } else {
521      return null;
522    }
523  }
524
525  private String lookupDisplay(Coding c) {
526    CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion(), valueset);
527    if (cs != null) {
528      ConceptDefinitionComponent cd = CodeSystemUtilities.findCodeOrAltCode(cs.getConcept(), c.getCode(), null);
529      if (cd != null) {
530        return getPreferredDisplay(cd, cs); 
531      }
532    }
533    return null;
534  }
535
536  public CodeSystem resolveCodeSystem(String system, String version, Resource source) {
537    for (CodeSystem t : localSystems) {
538      if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) {
539        return t;
540      }
541    }
542    CodeSystem cs = context.fetchSupplementedCodeSystem(system, version, source);
543    if (cs == null) {
544      cs = findSpecialCodeSystem(system, version);
545    }
546    if (cs == null) {
547      cs = context.findTxResource(CodeSystem.class, system, version, source);
548    }
549    if (cs != null) {
550      if (cs.hasUserData("supplements.installed")) {
551        for (String s : cs.getUserString("supplements.installed").split("\\,")) {
552          s = removeSupplement(s);
553        }
554      }
555    }
556
557    if (!requiredSupplements.isEmpty()) {
558      List<CodeSystem> additionalSupplements = new ArrayList<>();
559      for (String s : requiredSupplements) {
560        CodeSystem scs = context.findTxResource(CodeSystem.class, s);
561        if (scs != null && cs.getUrl().equals(scs.getSupplements())) {
562          additionalSupplements.add(scs);
563        }
564      }
565      if (!additionalSupplements.isEmpty()) {
566        cs = CodeSystemUtilities.mergeSupplements(cs, additionalSupplements);
567      }
568    }
569    return cs;
570  }
571
572  public Set<String> resolveCodeSystemVersions(String system) {
573    Set<String> res = new HashSet<>();
574    for (CodeSystem t : localSystems) {
575      if (t.getUrl().equals(system) && t.hasVersion()) {
576        res.add(t.getVersion());
577      }
578    }
579    res.addAll(new ContextUtilities(context).fetchCodeSystemVersions(system));
580    return res;
581  }
582
583  private boolean versionsMatch(String versionTest, String versionActual) {
584    return versionTest == null || versionActual == null || VersionUtilities.versionMatches(versionTest, versionActual);
585  }
586
587  public ValidationResult validateCode(Coding code) throws FHIRException {
588    return validateCode("Coding", code); 
589  }
590  
591  public ValidationResult validateCode(String path, Coding code) throws FHIRException {
592    opContext.deadCheck("validate "+code.toString());
593    checkValueSetOptions();
594    
595    String warningMessage = null;
596    // first, we validate the concept itself
597
598    ValidationResult res = null;
599    boolean inExpansion = false;
600    boolean inInclude = false;
601    List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
602    ValidationProcessInfo info = new ValidationProcessInfo(issues);
603    checkCanonical(issues, path, valueset, valueset);
604
605    String system = code.getSystem();
606    if (!options.isMembershipOnly()) {
607      if (system == null && !code.hasDisplay() && options.isGuessSystem()) { // dealing with just a plain code (enum)
608        List<StringWithCodes> problems = new ArrayList<>();
609        system = systemForCodeInValueSet(code.getCode(), problems);
610        if (system == null) {
611          if (problems.size() == 0) {
612            throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code
613          } else if (problems.size() == 1) {
614            String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
615            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, "code", msg, OpIssueCode.NotInVS, null, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE));
616            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, "code", problems.get(0).getMessage(), problems.get(0).getCode(), null, problems.get(0).getMessageId()));
617            return new ValidationResult(IssueSeverity.ERROR, problems.get(0).getMessage(), msg, issues);
618          } else {
619            CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("; ");
620            for (StringWithCodes s : problems) {
621              b.append(s.getMessage());
622            }            
623            ValidationResult vr = new ValidationResult(IssueSeverity.ERROR, b.toString(), null);
624            for (StringWithCodes s : problems) {
625              vr.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, path, s.getMessage(), s.getCode(), vr.getServer(), s.getMessageId()));
626            }
627            return vr;
628          }
629        }
630      }
631      if (!code.hasSystem()) {
632        if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) {
633          system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets
634        }
635        code.setSystem(system);
636      }
637      if (!code.hasSystem()) {
638        res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), null);
639        if (!code.hasUserData(UserDataNames.tx_val_sys_error)) {
640          code.setUserData(UserDataNames.tx_val_sys_error, true);
641          res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path, context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE), OpIssueCode.InvalidData, null, I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE));res.mineIssues(res.getIssues());
642          res.mineIssues(res.getIssues());
643        }
644      } else {
645        if (!Utilities.isAbsoluteUrl(system)) {
646          String msg = context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_RELATIVE);
647          issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".system", msg, OpIssueCode.InvalidData, null, I18nConstants.TERMINOLOGY_TX_SYSTEM_RELATIVE));
648        }
649        inExpansion = checkExpansion(code);
650        inInclude = checkInclude(code);
651        String workingVersion = getCodeSystemVersionFromValueSet(system, code.getCode());
652        CodeSystem csa = context.fetchCodeSystem(system); // get the latest
653        VersionAlgorithm va = csa == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(csa.getVersionAlgorithm());
654        String wv = determineVersion(path, system, workingVersion, code.getVersion(), issues, va);
655        CodeSystem cs = resolveCodeSystem(system, wv, null);
656        if (cs == null) {
657          if (!VersionUtilities.isR6Plus(context.getVersion()) && "urn:ietf:bcp:13".equals(system) && Utilities.existsInList(code.getCode(), "xml", "json", "ttl") && "http://hl7.org/fhir/ValueSet/mimetypes".equals(valueset.getUrl())) {
658            return new ValidationResult(system, null, new ConceptDefinitionComponent(code.getCode()), "application/fhir+"+code.getCode());        
659          } else {
660            OpIssueCode oic = OpIssueCode.NotFound;
661            IssueType itype = IssueType.NOTFOUND;
662            ValueSet vs = context.fetchResource(ValueSet.class, system);
663            String msgid = null;
664            if (vs != null) {
665              msgid = I18nConstants.TERMINOLOGY_TX_SYSTEM_VALUESET2;
666              warningMessage = context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_VALUESET2, system);  
667              oic = OpIssueCode.InvalidData;
668              itype = IssueType.INVALID;
669            } else if (wv == null) {
670              msgid = I18nConstants.UNKNOWN_CODESYSTEM;
671              warningMessage = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system);
672              unknownSystems.add(system);
673            } else {
674              msgid = getUnknownCodeSystemMessageId(system, wv);
675              warningMessage = getUnknownCodeSystemMessage(system, wv);
676            }
677            if (!inExpansion) {
678              if (valueset != null && valueset.hasExpansion()) {
679                String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION,
680                    valueset.getUrl(), 
681                    code.getSystem(), 
682                    code.getCode().toString());
683                issues.addAll(makeIssue(IssueSeverity.ERROR, itype, path, msg, OpIssueCode.VSProcessing, null, I18nConstants.CODESYSTEM_CS_UNK_EXPANSION));
684                throw new VSCheckerException(msg, issues, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
685              } else {
686                issues.addAll(makeIssue(IssueSeverity.ERROR, itype, path+".system", warningMessage, oic, null, msgid));
687                res = new ValidationResult(IssueSeverity.WARNING, warningMessage, issues);              
688                if (valueset == null) {
689                  throw new VSCheckerException(warningMessage, issues, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
690                } else {
691                  //              String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl(), code.toString());
692                  //              issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, msg));
693                  // we don't do this yet
694                  // throw new VSCheckerException(warningMessage, issues); 
695                }
696              }
697            }
698          }
699        } else {
700          checkCanonical(issues, path, cs, valueset);
701        }
702        if (cs != null && (cs.hasSupplements() || cs.getContent() == CodeSystemContentMode.SUPPLEMENT)) {
703          String msg = context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getVersionedUrl());
704          return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".system", msg, OpIssueCode.InvalidData, null, I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT));        
705        }
706        if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
707          warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition ";
708          switch (cs.getContent()) {
709          case EXAMPLE:
710            warningMessage = warningMessage +"only has example content";
711            break;
712          case FRAGMENT:
713            warningMessage = warningMessage + "is only a fragment";
714            break;
715          case NOTPRESENT:
716            warningMessage = warningMessage + "doesn't include any codes";
717            break;
718          case SUPPLEMENT:
719            warningMessage = warningMessage + " is for a supplement to "+cs.getSupplements();
720            break;
721          default:
722            break;
723          }
724          warningMessage = warningMessage + ", so the code has not been validated";
725          if (cs.getContent() == CodeSystemContentMode.NOTPRESENT) {
726            throw new VSCheckerException(warningMessage, null, TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
727          }
728          if (!options.isExampleOK() && !inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment
729            throw new VSCheckerException(warningMessage, null, true);
730          }
731        }
732
733        if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) {
734          if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT ||
735              (options.isExampleOK() && cs.getContent() == CodeSystemContentMode.EXAMPLE))) {
736            if (inInclude) {
737              ConceptReferenceComponent cc = findInInclude(code);
738              if (cc != null) {
739                // we'll take it on faith
740                String disp = getPreferredDisplay(cc);
741                res = new ValidationResult(system, cs.getVersion(), new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(disp), disp);
742                res.addMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct");
743                return res;
744              }
745            }
746            // we can't validate that here. 
747            throw new FHIRException("Unable to evaluate based on code system with status = "+cs.getContent().toCode());
748          }
749          res = validateCode(path, code, cs, null, info);
750          res.setIssues(issues);
751        } else if (cs == null && valueset.hasExpansion() && inExpansion) {
752          for (ValueSetExpansionParameterComponent p : valueset.getExpansion().getParameter()) {
753            if ("used-supplement".equals(p.getName())) {
754              removeSupplement(p.getValue().primitiveValue());
755            }
756          }
757          // we just take the value set as face value then
758          res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay());
759          if (!preferServerSide(system)) {
760            res.addMessage("Code System unknown, so assuming value set expansion is correct ("+warningMessage+")");
761          }
762        } else {
763          // well, we didn't find a code system - try the expansion? 
764          // disabled waiting for discussion
765          if (throwToServer) {
766            throw new FHIRException(NO_TRY_THE_SERVER);
767          }
768        }
769      }
770    } else {
771      inExpansion = checkExpansion(code);
772      inInclude = checkInclude(code);
773    }
774    String valueSetImpliedVersion = getCodeSystemVersionFromValueSet(system, code.getCode());
775    CodeSystem csa = context.fetchCodeSystem(system); // get the latest
776    VersionAlgorithm va = csa == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(csa.getVersionAlgorithm());
777    String wv = determineVersion(path, system, valueSetImpliedVersion, code.getVersion(), issues, va);
778    if (!checkRequiredSupplements(info)) {
779      return new ValidationResult(IssueSeverity.ERROR, issues.get(issues.size()-1).getDetails().getText(), issues);
780    }
781
782    
783    // then, if we have a value set, we check it's in the value set
784    if (valueset != null) {
785      if ((res==null || res.isOk())) { 
786        Boolean ok = codeInValueSet(path, system, wv, code.getCode(), info);
787        if (ok == null || !ok) {
788          if (res == null) {
789            res = new ValidationResult((IssueSeverity) null, null, info.getIssues());
790          } else {
791            res.mineIssues(info.getIssues());
792          }
793          if (info.getErr() != null) {
794            res.setErrorClass(info.getErr());
795          }
796          if (ok == null) {
797            String m = null;
798            String msgid = null;
799            if (!unknownSystems.isEmpty()) {
800//              msgid = I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS;
801//              m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_CS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(",", unknownSystems));
802            } else if (!unknownValueSets.isEmpty()) {
803//              msgid = I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS;
804//              res.addMessage(info.getIssues().get(0).getDetails().getText());
805//              m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_VS, valueset.getVersionedUrl(), CommaSeparatedStringBuilder.join(",", unknownValueSets));
806            } else {
807              // not sure why we'd get to here?
808//              msgid = I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_;
809//              m = context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl());
810            }
811            if (m != null) {
812              res.addMessage(m);
813              res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.NOTFOUND, null, m, OpIssueCode.VSProcessing, null, msgid));
814              res.mineIssues(res.getIssues());
815              res.setUnknownSystems(unknownSystems);
816              res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
817              res.setErrorClass(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
818            }
819          } else if (!inExpansion && !inInclude) {
820//            if (!info.getIssues().isEmpty()) {
821//              res.setMessage("Not in value set "+valueset.getUrl()+": "+info.summary()).setSeverity(IssueSeverity.ERROR);              
822//              res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path, res.getMessage()));
823//            } else
824//            {
825              String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
826              res.addMessage(msg).setSeverity(IssueSeverity.ERROR);
827              res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.NotInVS, null, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE));
828              res.mineIssues(res.getIssues());
829              res.setDefinition(null);
830              res.setSystem(null);
831              res.setDisplay(null);
832              res.setUnknownSystems(unknownSystems);              
833//            }
834          } else if (warningMessage!=null) {
835            String msg = context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage);
836            res = new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, msg, OpIssueCode.VSProcessing, null, I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_));
837          } else if (inExpansion) {
838            res.setMessage("Code found in expansion, however: " + res.getMessage());
839            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage(), OpIssueCode.VSProcessing, null, null)); // no message id?
840            res.mineIssues(res.getIssues());
841          } else if (inInclude) {
842            res.setMessage("Code found in include, however: " + res.getMessage());
843            res.getIssues().addAll(makeIssue(IssueSeverity.WARNING, IssueType.EXCEPTION, path, res.getMessage(), OpIssueCode.VSProcessing, null, null)); // no message id?
844            res.mineIssues(res.getIssues());
845          }
846        } else if (res == null) {
847          res = new ValidationResult(system, wv, null, null);
848        }
849      } else if ((res != null && !res.isOk())) {
850        String msg = context.formatMessagePlural(1, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getVersionedUrl(), "'"+code.toString()+"'");
851        res.addMessage(msg);
852        res.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path+".code", msg, OpIssueCode.NotInVS, null, I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_ONE));
853        res.mineIssues(res.getIssues());
854      }
855    }
856    if (res != null && res.isOk()) { // check that there aren't issues that should make it fail anyway
857      List<String> msgs = new ArrayList<>();
858      for (OperationOutcomeIssueComponent issue : issues) {
859        if (issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR) {
860          msgs.add(issue.getDetails().getText());
861        }
862      }
863      if (!msgs.isEmpty()) {
864        if (res.getMessage() == null) {
865          msgs.add(0, res.getMessage());
866        }
867        res.setSeverity(IssueSeverity.ERROR);
868        res.setMessage(CommaSeparatedStringBuilder.join("; ", msgs));
869      }
870    }
871    if (res != null && unknownSystems != null) {
872      res.setUnknownSystems(unknownSystems);
873      if (unknownSystemError != null) {
874        res.setErrorClass(unknownSystemError);
875      }
876    }
877
878    if (res != null && res.getSeverity() == IssueSeverity.INFORMATION && res.getMessage() != null) {
879      res.setSeverity(IssueSeverity.ERROR); // back patching for display logic issue
880    }
881    return res;
882  }
883
884  private String getCodeSystemVersionFromValueSet(String system, String code) {
885    if (valueset == null) {
886      return null;
887    }
888    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
889      if (inc.hasSystem() && inc.getSystem().equals(system) && inc.hasVersion()) {
890        boolean ok = true;
891        if (inc.hasConcept()) {
892          ok = false;
893          for (ConceptReferenceComponent c : inc.getConcept()) {
894            if (code.equals(c.getCode())) {
895              ok = true;
896              break;
897            }
898          }
899        }
900        if (ok) {
901          return inc.getVersion();
902        }
903      }
904    }
905    return null;
906  }
907
908  private void checkValueSetOptions() {
909    if (valueset != null) {
910      for (Extension ext : valueset.getCompose().getExtensionsByUrl(ExtensionDefinitions.EXT_VS_EXP_PARAM_NEW, ExtensionDefinitions.EXT_VS_EXP_PARAM_OLD)) {
911        var name = ext.getExtensionString("name");
912        var value = ext.getExtensionByUrl("value").getValue();
913        if ("displayLanguage".equals(name)) {
914          options.setLanguages(value.primitiveValue());
915        }
916      }
917      if (!options.hasLanguages() && valueset.hasLanguage()) {
918        options.addLanguage(valueset.getLanguage());
919      }
920    }
921
922    if (options.getLanguages() != null) {
923      for (LanguagePreference t : options.getLanguages().getLangs()) {
924        try {
925          LanguageTag tag = new LanguageTag(registry, t.getLang());
926        } catch (Exception e) {
927          throw new TerminologyServiceProtectionException(context.formatMessage(I18nConstants.INVALID_DISPLAY_NAME, options.getLanguages().getSource()), TerminologyServiceErrorClass.PROCESSING, IssueType.PROCESSING, e.getMessage());
928        }
929      }
930    }
931  }
932
933  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", 
934      "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/",
935      "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"));
936  
937  private boolean preferServerSide(String system) {
938    if (SERVER_SIDE_LIST.contains(system)) {
939      return true;
940    }
941    
942    try {
943      if (tcm.supportsSystem(system)) {
944        return true;
945      }
946    } catch (IOException e) {
947      e.printStackTrace();
948    }
949  
950    return false;    
951  }
952
953  private boolean checkInclude(Coding code) {
954    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
955      return false;
956    }
957    for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
958      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
959        for (ConceptReferenceComponent cc : inc.getConcept()) {
960          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
961            return false;
962          }
963        }
964      }
965    }
966    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
967      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
968        for (ConceptReferenceComponent cc : inc.getConcept()) {
969          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
970            return true;
971          }
972        }
973      }
974    }
975    return false;
976  }
977
978  private ConceptReferenceComponent findInInclude(Coding code) {
979    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
980      return null;
981    }
982    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
983      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
984        for (ConceptReferenceComponent cc : inc.getConcept()) {
985          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
986            return cc;
987          }
988        }
989      }
990    }
991    return null;
992  }
993
994  private CodeSystem findSpecialCodeSystem(String system, String version) {
995    if ("urn:ietf:rfc:3986".equals(system)) {
996      CodeSystem cs = new CodeSystem();
997      cs.setUrl(system);
998      cs.setUserData(UserDataNames.tx_cs_special, new URICodeSystem());
999      cs.setContent(CodeSystemContentMode.COMPLETE);
1000      return cs; 
1001    }
1002    if (Utilities.isAbsoluteUrl(system)) {
1003      StructureDefinition sd = context.fetchResource(StructureDefinition.class, system, version, null);
1004      if (sd != null) {
1005        return CodeSystemUtilities.convertSD(sd);
1006      }
1007    }
1008    return null;
1009  }
1010
1011  private ValidationResult findCodeInExpansion(Coding code) {
1012    if (valueset==null || !valueset.hasExpansion())
1013      return null;
1014    return findCodeInExpansion(code, valueset.getExpansion().getContains());
1015  }
1016
1017  private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
1018    for (ValueSetExpansionContainsComponent containsComponent: contains) {
1019      opContext.deadCheck("findCodeInExpansion");
1020      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
1021        ConceptDefinitionComponent ccd = new ConceptDefinitionComponent();
1022        ccd.setCode(containsComponent.getCode());
1023        ccd.setDisplay(containsComponent.getDisplay());
1024        ValidationResult res = new ValidationResult(code.getSystem(), code.hasVersion() ? code.getVersion() : containsComponent.getVersion(), ccd, getPreferredDisplay(ccd, null));
1025        return res;
1026      }
1027      if (containsComponent.hasContains()) {
1028        ValidationResult res = findCodeInExpansion(code, containsComponent.getContains());
1029        if (res != null) {
1030          return res;
1031        }
1032      }
1033    }
1034    return null;
1035  }
1036
1037  private boolean checkExpansion(Coding code) {
1038    if (valueset==null || !valueset.hasExpansion()) {
1039      return false;
1040    }
1041    return checkExpansion(code, valueset.getExpansion().getContains());
1042  }
1043
1044  private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
1045    for (ValueSetExpansionContainsComponent containsComponent: contains) {
1046      opContext.deadCheck("checkExpansion: "+code.toString());
1047      if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
1048        return true;
1049      }
1050      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) {
1051        return true;
1052      }
1053    }
1054    return false;
1055  }
1056
1057  private ValidationResult validateCode(String path, Coding code, CodeSystem cs, CodeableConcept vcc, ValidationProcessInfo info) {
1058    if (code.getCode() == null) {
1059      String msgid = cs.getVersion() == null ? I18nConstants.NO_CODE_PROVIDED : I18nConstants.NO_CODE_PROVIDED_VERSION;
1060      String msg = context.formatMessage(msgid, cs.getUrl(), cs.getVersion());
1061      return new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.VALUE, path + ".code", msg, OpIssueCode.InvalidData, null, msgid));
1062    }
1063    ConceptDefinitionComponent cc = cs.hasUserData(UserDataNames.tx_cs_special) ? ((SpecialCodeSystem) cs.getUserData(UserDataNames.tx_cs_special)).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode(), cs.getCaseSensitive(), allAltCodes);
1064    if (cc == null) {
1065      cc = findSpecialConcept(code, cs);
1066    }
1067    if (cc == null) {
1068      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
1069        String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_FRAGMENT, code.getCode(), cs.getUrl(), cs.getVersion());
1070        return new ValidationResult(IssueSeverity.WARNING, msg, makeIssue(IssueSeverity.WARNING, IssueType.CODEINVALID, path + ".code", msg, OpIssueCode.InvalidCode, null, I18nConstants.UNKNOWN_CODE_IN_FRAGMENT));
1071      } else {
1072        String msg = context.formatMessage(I18nConstants.UNKNOWN_CODE_IN_VERSION, code.getCode(), cs.getUrl(), cs.getVersion());
1073        return new ValidationResult(IssueSeverity.ERROR, msg, makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path + ".code", msg, OpIssueCode.InvalidCode, null, I18nConstants.UNKNOWN_CODE_IN_VERSION));
1074      }
1075    } else {
1076      if (!cc.getCode().equals(code.getCode())) {
1077        String msg = context.formatMessage(I18nConstants.CODE_CASE_DIFFERENCE, code.getCode(), cc.getCode(), cs.getVersionedUrl());
1078        info.addIssue(makeIssue(IssueSeverity.INFORMATION, IssueType.BUSINESSRULE, path + ".code", msg, OpIssueCode.CodeRule, null, I18nConstants.CODE_CASE_DIFFERENCE));
1079      }
1080    }
1081    Coding vc = new Coding().setCode(cc.getCode()).setSystem(cs.getUrl()).setVersion(cs.getVersion()).setDisplay(getPreferredDisplay(cc, cs));
1082    if (vcc != null) {
1083      vcc.addCoding(vc);
1084    }
1085
1086    boolean inactive = (CodeSystemUtilities.isInactive(cs, cc));
1087    String status = CodeSystemUtilities.getStatus(cs, cc);
1088
1089    String statusMessage = null;
1090    if (inactive) {
1091      statusMessage = context.formatMessage(I18nConstants.INACTIVE_CONCEPT_FOUND, status == null ? "inactive" : status, cc.getCode());
1092      info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.BUSINESSRULE, path, statusMessage, OpIssueCode.CodeComment, null, I18nConstants.INACTIVE_CONCEPT_FOUND));
1093    } else if (status != null && "deprecated".equals(status.toLowerCase())) {
1094      statusMessage = context.formatMessage(I18nConstants.DEPRECATED_CONCEPT_FOUND, status == null ? "inactive" : status, cc.getCode());
1095      info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.BUSINESSRULE, path, statusMessage, OpIssueCode.CodeComment, null, I18nConstants.DEPRECATED_CONCEPT_FOUND));
1096    }
1097    boolean isDefaultLang = false;
1098    boolean ws = false;     
1099    if (code.getDisplay() == null) {
1100      if (statusMessage == null) {
1101        return new ValidationResult(code.getSystem(), cs.getVersion(), cc, vc.getDisplay()).setStatus(inactive, status);
1102      } else {
1103        return new ValidationResult(IssueSeverity.WARNING, statusMessage, code.getSystem(), cs.getVersion(), cc, vc.getDisplay(), null).setStatus(inactive, status);
1104      }
1105    }
1106    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(", ", " or ");
1107    if (cc.hasDisplay() && isOkLanguage(cs.getLanguage())) {
1108      b.append("'"+cc.getDisplay()+"'"+(cs.hasLanguage() ? " ("+cs.getLanguage()+")" : ""));
1109      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
1110        return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
1111      } else if (Utilities.normalize(code.getDisplay()).equals(Utilities.normalize(cc.getDisplay()))) {
1112        ws = true;
1113      }
1114    } else if (cc.hasDisplay() && code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
1115      isDefaultLang = true;
1116    }
1117    
1118    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
1119      opContext.deadCheck("validateCode1 "+ds.toString());
1120      if (isOkLanguage(ds.getLanguage())) {
1121        b.append("'"+ds.getValue()+"' ("+ds.getLanguage()+")");
1122        if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
1123          if (ds.hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status")) {
1124            String dstatus = ds.getExtensionString("http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status");
1125            List<String> ok = new ArrayList<>();
1126            ok.add(cc.getDisplay());
1127            for (ConceptDefinitionDesignationComponent ds1 : cc.getDesignation()) {
1128              if (isOkLanguage(ds.getLanguage()) && !ds.hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status")) {
1129                ok.add(ds.getValue());
1130              }
1131            }
1132            Collections.sort(ok);
1133            String msg = context.formatMessagePlural(ok.size(), I18nConstants.INACTIVE_DISPLAY_FOUND, code.getDisplay(), cc.getCode(), CommaSeparatedStringBuilder.join(", ", ok), dstatus);
1134            info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg, OpIssueCode.DisplayComment, null, I18nConstants.INACTIVE_DISPLAY_FOUND));
1135          }
1136          return new ValidationResult(code.getSystem(),cs.getVersion(),  cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
1137        }
1138        if (Utilities.normalize(code.getDisplay()).equalsIgnoreCase(Utilities.normalize(ds.getValue()))) {
1139          ws = true;
1140        }
1141      }
1142    }
1143    // also check to see if the value set has another display
1144    if (options.isUseValueSetDisplays()) {
1145      ConceptReferencePair vs = findValueSetRef(code.getSystem(), code.getCode());
1146      if (vs != null && (vs.getCc().hasDisplay() ||vs.getCc().hasDesignation())) {
1147        if (vs.getCc().hasDisplay() && isOkLanguage(vs.getValueset().getLanguage())) {
1148          b.append("'"+vs.getCc().getDisplay()+"'");
1149          if (code.getDisplay().equalsIgnoreCase(vs.getCc().getDisplay())) {
1150            return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
1151          }
1152        }
1153        for (ConceptReferenceDesignationComponent ds : vs.getCc().getDesignation()) {
1154          opContext.deadCheck("validateCode2 "+ds.toString());
1155          if (isOkLanguage(ds.getLanguage())) {
1156            b.append("'"+ds.getValue()+"'");
1157            if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
1158              return new ValidationResult(code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs)).setStatus(inactive, status);
1159            }
1160          }
1161        }
1162      }
1163    }
1164    if (b.count() > 0) {
1165      String msgId = ws ? I18nConstants.DISPLAY_NAME_WS_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF : I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF;
1166      String msg = context.formatMessagePlural(b.count(), msgId, code.getSystem(), code.getCode(), b.toString(), code.getDisplay(), options.langSummary());
1167      return new ValidationResult(dispWarningStatus(), msg, code.getSystem(), cs.getVersion(), cc, getPreferredDisplay(cc, cs), makeIssue(dispWarning(), IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null, msgId)).setStatus(inactive, status);
1168    } else if (isDefaultLang) {
1169      // 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
1170      boolean none = options.getLanguages().getLangs().size() == 1 && !hasLanguage(cs, options.getLanguages().getLangs().get(0));
1171      String msgid = none ? I18nConstants.NO_VALID_DISPLAY_FOUND_LANG_NONE : I18nConstants.NO_VALID_DISPLAY_FOUND_LANG_SOME;
1172      String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), msgid, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary(), code.getDisplay());
1173      String n = null;
1174      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, msgid)).setStatus(inactive, status);
1175    } else if (!code.getDisplay().equals(vc.getDisplay())) {
1176      String msg = context.formatMessage(I18nConstants.NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_ERR, code.getDisplay(), code.getSystem(), code.getCode(), options.langSummary(), vc.getDisplay());
1177      return new ValidationResult(IssueSeverity.ERROR, msg, code.getSystem(), cs.getVersion(), cc, cc.getDisplay(), makeIssue(dispWarning(), IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null, I18nConstants.NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_ERR)).setStatus(inactive, status).setErrorIsDisplayIssue(true);
1178    } else {
1179      String msg = context.formatMessagePlural(options.getLanguages().getLangs().size(), I18nConstants.NO_VALID_DISPLAY_FOUND, code.getSystem(), code.getCode(), code.getDisplay(), options.langSummary());
1180      return new ValidationResult(IssueSeverity.WARNING, msg, code.getSystem(), cs.getVersion(), cc, cc.getDisplay(), makeIssue(IssueSeverity.WARNING, IssueType.INVALID, path+".display", msg, OpIssueCode.Display, null, I18nConstants.NO_VALID_DISPLAY_FOUND)).setStatus(inactive, status);
1181    }
1182  }
1183
1184  private boolean hasLanguage(CodeSystem cs, LanguagePreference languagePreference) {
1185    String lang = languagePreference.getLang();
1186    if (lang == null) {
1187      return false;
1188    }
1189    for (ConceptDefinitionComponent cc : cs.getConcept()) {
1190      boolean hl = hasLanguage(cs, cc, lang);
1191      if (hl) {
1192        return true;
1193      }
1194    }
1195    return false;
1196  }
1197
1198  private boolean hasLanguage(CodeSystem cs, ConceptDefinitionComponent cc, String lang) {
1199    if (lang.equals(cs.getLanguage()) && cc.hasDisplay()) {
1200      return true;
1201    }
1202    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
1203      if (lang.equals(d.getLanguage())) {
1204        return true;
1205      }
1206    }
1207    for (ConceptDefinitionComponent cc1 : cc.getConcept()) {
1208      boolean hl = hasLanguage(cs, cc1, lang);
1209      if (hl) {
1210        return true;
1211      }
1212    }
1213    return false;
1214  }
1215
1216  private ConceptDefinitionComponent findSpecialConcept(Coding c, CodeSystem cs) {
1217    // handling weird special cases in v2 code systems 
1218    if ("http://terminology.hl7.org/CodeSystem/v2-0203".equals(cs.getUrl())) {
1219      String code = c.getCode();
1220      if (code != null && code.startsWith("NN") && code.length() > 3) {
1221        ConceptDefinitionComponent cd = findCountryCode(code.substring(2));
1222        if (cd != null) {
1223          return new ConceptDefinitionComponent(code).setDisplay("National Identifier for "+cd.getDisplay());
1224        }
1225      }      
1226    }
1227//    0396: HL7nnnn, IBTnnnn, ISOnnnn, X12Dennnn, 99zzz
1228//    0335: PRNxxx
1229    return null;
1230  }
1231
1232  
1233  private ConceptDefinitionComponent findCountryCode(String code) {
1234    ValidationResult vr = context.validateCode(new ValidationOptions(FhirPublication.R5), "urn:iso:std:iso:3166", null, code, null);
1235    return vr == null || !vr.isOk() ? null : new ConceptDefinitionComponent(code).setDisplay(vr.getDisplay()).setDefinition(vr.getDefinition());
1236  }
1237
1238  private IssueSeverity dispWarning() {
1239    return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.ERROR; 
1240  }
1241  
1242  private IssueSeverity dispWarningStatus() {
1243    return options.isDisplayWarningMode() ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; // information -> error later
1244  }
1245
1246  private boolean isOkLanguage(String language) {
1247    if (!options.hasLanguages()) {
1248      return true;
1249    }
1250    if (LanguageUtils.langsMatch(options.getLanguages(), language)) {
1251      return true;
1252    }
1253    if (language == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US") || options.isEnglishOk())) {
1254      return true;
1255    }
1256    return false;
1257  }
1258
1259  private ConceptReferencePair findValueSetRef(String system, String code) {
1260    if (valueset == null)
1261      return null;
1262    // if it has an expansion
1263    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
1264      opContext.deadCheck("findValueSetRef "+exp.toString());
1265      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
1266        ConceptReferenceComponent cc = new ConceptReferenceComponent();
1267        cc.setDisplay(exp.getDisplay());
1268        cc.setDesignation(exp.getDesignation());
1269        return new ConceptReferencePair(valueset, cc);
1270      }
1271    }
1272    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
1273      if (system.equals(inc.getSystem())) {
1274        for (ConceptReferenceComponent cc : inc.getConcept()) {
1275          if (cc.getCode().equals(code)) {
1276            return new ConceptReferencePair(valueset, cc);
1277          }
1278        }
1279      }
1280      for (CanonicalType url : inc.getValueSet()) {
1281        ConceptReferencePair cc = getVs(getCu().pinValueSet(url.asStringValue(), expansionParameters), null).findValueSetRef(system, code);
1282        if (cc != null) {
1283          return cc;
1284        }
1285      }
1286    }
1287    return null;
1288  }
1289
1290  /*
1291   * Check that all system values within an expansion correspond to the specified system value
1292   */
1293  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
1294    for (ValueSetExpansionContainsComponent contains : containsList) {
1295      if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) {
1296        return false;
1297      }
1298    }
1299    return true;
1300  }
1301
1302  private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) {
1303    opContext.deadCheck("findCodeInConcept: "+code.toString()+", "+concept.toString());
1304    if (code.equals(concept.getCode())) {
1305      return concept;
1306    }
1307    ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code, caseSensitive, altCodeRules);
1308    if (cc != null) {
1309      return cc;
1310    }
1311    if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
1312      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
1313      for (ConceptDefinitionComponent c : children) {
1314        cc = findCodeInConcept(c, code, caseSensitive, altCodeRules);
1315        if (cc != null) {
1316          return cc;
1317        }
1318      }
1319    }
1320    return null;
1321  }
1322  
1323  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code, boolean caseSensitive, AlternateCodesProcessingRules altCodeRules) {
1324    if (code == null) {
1325      return null;
1326    }
1327    for (ConceptDefinitionComponent cc : concept) {
1328      if (code.equals(cc.getCode()) || (!caseSensitive && (code.equalsIgnoreCase(cc.getCode())))) {
1329        return cc;
1330      }
1331      if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
1332        return cc;
1333      }
1334      ConceptDefinitionComponent c = findCodeInConcept(cc, code, caseSensitive, altCodeRules);
1335      if (c != null) {
1336        return c;
1337      }
1338    }
1339    return null;
1340  }
1341
1342
1343  private List<String> alternateCodes(ConceptDefinitionComponent focus, AlternateCodesProcessingRules altCodeRules) {
1344    List<String> codes = new ArrayList<>();
1345    for (ConceptPropertyComponent p : focus.getProperty()) {
1346      if ("alternateCode".equals(p.getCode()) && (altCodeRules.passes(p.getExtension())) && p.getValue().isPrimitive()) {
1347        codes.add(p.getValue().primitiveValue());        
1348      }
1349    }
1350    return codes;
1351  }
1352
1353  
1354  private String systemForCodeInValueSet(String code, List<StringWithCodes> problems) {
1355    Set<String> sys = new HashSet<>();
1356    if (!scanForCodeInValueSet(code, sys, problems)) {
1357      return null;
1358    }
1359    if (sys.size() == 0) {
1360      problems.add(new StringWithCodes(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_INFER_CODESYSTEM, code, valueset.getVersionedUrl()), I18nConstants.UNABLE_TO_INFER_CODESYSTEM));
1361      return null;
1362    } else if (sys.size() > 1) {
1363      problems.add(new StringWithCodes(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, code, valueset.getVersionedUrl(), sys.toString()), I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES));
1364      return null;
1365    } else {
1366      return sys.iterator().next();
1367    }
1368  }
1369  
1370  private boolean scanForCodeInValueSet(String code, Set<String> sys, List<StringWithCodes> problems) {
1371    if (valueset.hasCompose()) {
1372      //  ignore excludes - they can't make any difference
1373      if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) {
1374        problems.add(new StringWithCodes(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, code, valueset.getVersionedUrl()), I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION));
1375      }
1376
1377      int i = 0;
1378      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
1379        opContext.deadCheck("scanForCodeInValueSet: "+code.toString());
1380        if (scanForCodeInValueSetInclude(code, sys, problems, i, vsi)) {
1381          return true;
1382        }
1383        i++;
1384      }
1385    } else if (valueset.hasExpansion()) {
1386      // Retrieve a list of all systems associated with this code in the expansion
1387      if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) {
1388        return false;
1389      }
1390    }
1391    return true;
1392  }
1393
1394  private boolean scanForCodeInValueSetInclude(String code, Set<String> sys, List<StringWithCodes> problems, int i, ConceptSetComponent vsi) {
1395    if (vsi.hasValueSet()) {
1396      for (CanonicalType u : vsi.getValueSet()) {
1397        if (!checkForCodeInValueSet(code, getCu().pinValueSet(u.getValue(), expansionParameters), sys, problems)) {
1398          return false;
1399        }
1400      }
1401    } else if (!vsi.hasSystem()) { 
1402      problems.add(new StringWithCodes(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, code, valueset.getVersionedUrl(), i), I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM));
1403      return false;
1404    }
1405    if (vsi.hasSystem()) {
1406      if (vsi.hasFilter()) {
1407        ValueSet vsDummy = new ValueSet();
1408        vsDummy.setUrl(UUIDUtilities.makeUuidUrn());
1409        vsDummy.setStatus(PublicationStatus.ACTIVE);
1410        vsDummy.getCompose().addInclude(vsi);
1411        Coding c = new Coding().setCode(code).setSystem(vsi.getSystem());
1412        ValidationResult vr = context.validateCode(options.withGuessSystem(false), c, vsDummy);
1413        if (vr.isOk()) {
1414          sys.add(vsi.getSystem());
1415        } else {
1416          // 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))));
1417          return false;
1418        }
1419      }
1420      CodeSystemProvider csp = CodeSystemProvider.factory(vsi.getSystem());
1421      if (csp != null) {
1422        Boolean ok = csp.checkCode(code);
1423        if (ok == null) {
1424          problems.add(new StringWithCodes(OpIssueCode.InferFailed, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE, code, valueset.getVersionedUrl(), vsi.getSystem()), I18nConstants.UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE));
1425          sys.add(vsi.getSystem());
1426        } else if (ok) {
1427          sys.add(vsi.getSystem());
1428        }
1429      } else {
1430        CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion(), valueset);
1431        if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) {
1432          if (vsi.hasConcept()) {
1433            for (ConceptReferenceComponent cc : vsi.getConcept()) {
1434              boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
1435              if (match) {
1436                sys.add(vsi.getSystem());
1437              }
1438            }
1439          } else {
1440            ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code, cs.getCaseSensitive(), allAltCodes);
1441            if (cc != null) {
1442              sys.add(vsi.getSystem());
1443            }
1444          }
1445        } else if (vsi.hasConcept()) {
1446          for (ConceptReferenceComponent cc : vsi.getConcept()) {
1447            boolean match = cc.getCode().equals(code);
1448            if (match) {
1449              sys.add(vsi.getSystem());
1450            }
1451          }
1452        } else if (!VersionUtilities.isR6Plus(context.getVersion()) && Utilities.existsInList(code, "xml", "json", "ttl") && "urn:ietf:bcp:13".equals(vsi.getSystem())) {
1453          sys.add(vsi.getSystem());
1454          return true;
1455        } else {
1456          ValueSet vsDummy = new ValueSet();
1457          vsDummy.setUrl(UUIDUtilities.makeUuidUrn());
1458          vsDummy.setStatus(PublicationStatus.ACTIVE);
1459          vsDummy.getCompose().addInclude(vsi);
1460          ValidationResult vr = context.validateCode(options.withNoClient(), code, vsDummy);
1461          if (vr.isOk()) {
1462            sys.add(vsi.getSystem());
1463          } else {
1464            // ok, we'll try to expand this one then 
1465            ValueSetExpansionOutcome vse = context.expandVS(new TerminologyOperationDetails(requiredSupplements), vsi, false, false);
1466            if (vse.isOk()) {
1467              if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) {
1468                return false;
1469              }
1470            } else {
1471              problems.add(new StringWithCodes(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()), I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM));
1472              return false;
1473            }
1474
1475          }
1476        }
1477      }
1478    }
1479    return false;
1480  }
1481
1482  private String filterSummary(ConceptSetComponent vsi) {
1483    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1484    for (ConceptSetFilterComponent f : vsi.getFilter()) {
1485      b.append(f.getProperty()+f.getOp().toCode()+f.getValue());
1486    }
1487    return b.toString();
1488  }
1489
1490  private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<StringWithCodes> problems) {
1491    ValueSetValidator vs = getVs(uri, null);
1492    return vs.scanForCodeInValueSet(code, sys, problems);
1493  }
1494
1495  /*
1496   * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding
1497   * to the passed list. 
1498   */
1499  private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<StringWithCodes> problems) {
1500    for (ValueSetExpansionContainsComponent c: contains) {
1501      opContext.deadCheck("checkSystems "+code.toString());
1502      if (c.getCode().equals(code)) {
1503        systems.add(c.getSystem());
1504      }
1505      if (c.hasContains())
1506        checkSystems(c.getContains(), code, systems, problems);
1507    }
1508    return true;
1509  }
1510  
1511  public Boolean codeInValueSet(String path, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
1512    if (valueset == null) {
1513      return null;
1514    }
1515    opContext.deadCheck("codeInValueSet: "+system+"#"+code);
1516    checkCanonical(info.getIssues(), path, valueset, valueset);
1517    Boolean result = false;
1518    String vspath = "ValueSet['"+valueset.getVersionedUrl()+"].compose";
1519      
1520    if (valueset.hasExpansion()) {
1521      return checkExpansion(new Coding(system, code, null));
1522    } else if (valueset.hasCompose()) {
1523      int i = 0;
1524      int c = 0;
1525      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
1526        Boolean ok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info, vspath+".include["+c+"]", info.getIssues());
1527        i++;
1528        c++;
1529        if (ok == null && result != null && result == false) {
1530          result = null;
1531        } else if (ok != null && ok) {
1532          result = true;
1533          break;
1534        }
1535      }
1536      i = valueset.getCompose().getInclude().size();
1537      c = 0;
1538      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
1539        Boolean nok = inComponent(path, vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info, vspath+".exclude["+c+"]", info.getIssues());
1540        i++;
1541        c++;
1542        if (nok == null && result != null && result == false) {
1543          result = null;
1544        } else if (nok != null && nok) {
1545          result = false;
1546        }
1547      }
1548    } 
1549
1550    return result;
1551  }
1552
1553  private String determineVersion(String path, @Nonnull String system, String versionVS, String versionCoding, List<OperationOutcomeIssueComponent> issues, VersionAlgorithm va) {
1554    Resource source = valueset;
1555
1556    // phase 1: get correct system version
1557    String result = determineVersion(system, versionVS, va);
1558
1559    if (!Utilities.noString(versionCoding)) {
1560      // phase 3: figure out the correct code system version
1561      // if versionCoding is more detailed than result, then we use that instead
1562      if (versionIsMoreDetailed(va, result, versionCoding)) {
1563        result = versionCoding;
1564        source = null;
1565      }
1566      String key = system+"^"+result+"^"+versionVS+"^"+versionCoding;
1567      if (!checkedVersionCombinations.contains(key)) {
1568        checkedVersionCombinations.add(key);
1569
1570        // phase 4: is the determined version compatible?
1571        CodeSystem cs = context.fetchResource(CodeSystem.class, system, result, source);
1572        if (cs != null) {
1573          checkVersion(path, system, cs.getVersion(), va, issues);
1574        }
1575        if (cs != null && !(versionCoding.equals(cs.getVersion()) || versionIsMoreDetailed(va, versionCoding, cs.getVersion()))) {
1576          if (result == null) {
1577            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, Utilities.noString(path) ? "version" : path + "." + "version",
1578              context.formatMessage(I18nConstants.VALUESET_VALUE_MISMATCH_DEFAULT, system, cs.getVersion(), notNull(versionVS), notNull(versionCoding)), OpIssueCode.VSProcessing, null, I18nConstants.VALUESET_VALUE_MISMATCH_DEFAULT));
1579          } else if (!result.equals(versionVS)) {
1580            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, Utilities.noString(path) ? "version" : path + "." + "version",
1581              context.formatMessage(I18nConstants.VALUESET_VALUE_MISMATCH_CHANGED, system, result, notNull(versionVS), notNull(versionCoding)), OpIssueCode.VSProcessing, null, I18nConstants.VALUESET_VALUE_MISMATCH_CHANGED));
1582          } else if (!notNull(versionVS).equals(notNull(versionCoding))) {
1583            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, Utilities.noString(path) ? "version" : path + "." + "version",
1584              context.formatMessage(I18nConstants.VALUESET_VALUE_MISMATCH, system, notNull(versionVS), notNull(versionCoding)), OpIssueCode.VSProcessing, null, I18nConstants.VALUESET_VALUE_MISMATCH));
1585          }
1586          CodeSystem cs2 = context.fetchCodeSystem(system, versionCoding, source);
1587          if (cs2 == null) {
1588            List<CodeSystem> list = context.fetchResourceVersions(CodeSystem.class, system);
1589            Set<String> versions = new HashSet<>();
1590            for (CodeSystem c : list) {
1591              versions.add(c.getVersion());
1592            }
1593            if (!unknownSystems.contains(system + '|' + versionCoding)) {
1594              unknownSystemError = TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED;
1595              unknownSystems.add(system + '|' + versionCoding);
1596              issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, Utilities.noString(path) ? "version" : path + "." + "system",
1597                context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, versionCoding, CommaSeparatedStringBuilder.join(",", Utilities.sorted(versions))), OpIssueCode.NotFound, null, I18nConstants.UNKNOWN_CODESYSTEM_VERSION));
1598            }
1599          }
1600        } else if (cs == null && result != null && !versionCoding.equals(result)) {
1601          issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, Utilities.noString(path) ? "version" : path + "." + "version",
1602            context.formatMessage(I18nConstants.VALUESET_VALUE_MISMATCH, system, result, versionCoding), OpIssueCode.VSProcessing, null, I18nConstants.VALUESET_VALUE_MISMATCH));
1603        }
1604      }
1605    }
1606    return result;
1607  }
1608
1609  private Object notNull(String s) {
1610    return s == null ? "" : s;
1611  }
1612
1613  private boolean versionIsMoreDetailed(VersionAlgorithm va, String criteria, String candidate) {
1614    if (va == VersionAlgorithm.Unknown) {
1615      va = VersionAlgorithm.guessFormat(candidate);
1616    }
1617    switch (va) {
1618      case SemVer:
1619        return  VersionUtilities.isSemVerWithWildcards(criteria) && VersionUtilities.isSemVer(candidate) && VersionUtilities.versionMatches(criteria, candidate);
1620      default:
1621        return candidate.startsWith(criteria);
1622    }
1623  }
1624
1625  private String determineVersion(@Nonnull String system, @Nonnull String version, VersionAlgorithm va) {
1626    String result = version;
1627    List<Parameters.ParametersParameterComponent> rules = new ArrayList<>();
1628    for (Parameters.ParametersParameterComponent p : expansionParameters.getParameter()) {
1629      if ("force-system-version".equalsIgnoreCase(p.getName()) && p.getValue().primitiveValue() != null && p.getValue().primitiveValue().startsWith(system+"|")) {
1630        rules.add(p);
1631      }
1632      if ("system-version".equalsIgnoreCase(p.getName()) && p.getValue().primitiveValue() != null && p.getValue().primitiveValue().startsWith(system+"|")) {
1633        rules.add(p);
1634      }
1635      if ("check-system-version".equalsIgnoreCase(p.getName()) && p.getValue().primitiveValue() != null && p.getValue().primitiveValue().startsWith(system+"|")) {
1636        rules.add(p);
1637      }
1638    }
1639    boolean b = false;
1640    for (Parameters.ParametersParameterComponent t : rules) {
1641      if ("force-system-version".equals(t.getName())) {
1642        String tv = t.getValue().primitiveValue().substring(system.length() + 1);
1643        if (!b) {
1644          result = tv;
1645        } else if (!tv.equals(result)) {
1646          throw failWithIssue(IssueType.EXCEPTION, OpIssueCode.VersionError, null, I18nConstants.SYSTEM_VERSION_MULTIPLE_OVERRIDE, system, result, tv);
1647        }
1648      }
1649    }
1650    if (Utilities.noString(result)) {
1651      b = false;
1652      for (Parameters.ParametersParameterComponent t : rules) {
1653        if ("system-version".equals(t.getName())) {
1654          String tv = t.getValue().primitiveValue().substring(system.length() + 1);
1655          if (!b) {
1656            result = tv;
1657          } else if (!tv.equals(result)) {
1658            throw failWithIssue(IssueType.EXCEPTION, OpIssueCode.VersionError, null, I18nConstants.SYSTEM_VERSION_MULTIPLE_DEFAULT, system, result, tv);
1659          }
1660        }
1661      }
1662    }
1663    for (Parameters.ParametersParameterComponent t : rules) {
1664      if ("check-system-version".equals(t.getName())) {
1665        String tv = t.getValue().primitiveValue().substring(system.length() + 1);
1666        if (Utilities.noString(result)) {
1667          result = tv;
1668        }
1669      }
1670    }
1671    return result;
1672  }
1673
1674  private void checkVersion(String path, @Nonnull String system, @Nonnull String version, VersionAlgorithm va, List<OperationOutcomeIssueComponent> issues) {
1675    List<Parameters.ParametersParameterComponent> rules = new ArrayList<>();
1676    for (Parameters.ParametersParameterComponent p : expansionParameters.getParameter()) {
1677      if ("check-system-version".equalsIgnoreCase(p.getName()) && p.getValue().primitiveValue() != null && p.getValue().primitiveValue().startsWith(system+"|")) {
1678        rules.add(p);
1679      }
1680    }
1681    for (Parameters.ParametersParameterComponent t : rules) {
1682      String tv = t.getValue().primitiveValue().substring(system.length() + 1);
1683      if (!versionsMatch(system, version, tv)) {
1684        if (issues == null) {
1685          throw failWithIssue(IssueType.EXCEPTION, OpIssueCode.VersionError, null, I18nConstants.VALUESET_VERSION_CHECK, system, version, tv);
1686        } else {
1687          String msg = context.formatMessage(I18nConstants.VALUESET_VERSION_CHECK, system, version, tv);
1688          if (!hasIssue(issues, msg)) {
1689             issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.EXCEPTION, Utilities.noString(path) ? "version" : path + "." + "version", msg, OpIssueCode.VersionError, null, I18nConstants.VALUESET_VERSION_CHECK));
1690          }
1691        }
1692      }
1693    }
1694  }
1695
1696  private boolean hasIssue(List<OperationOutcomeIssueComponent> issues, String msg) {
1697    for (OperationOutcomeIssueComponent t : issues) {
1698      if (msg.equals(t.getDetails().getText())) {
1699        return true;
1700      }
1701    }
1702    return false;
1703  }
1704
1705  private Boolean inComponent(String path, ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info, String vspath, List<OperationOutcomeIssueComponent> issues) throws FHIRException {
1706    opContext.deadCheck("inComponent "+vsiIndex);
1707    boolean ok = true;
1708    
1709    if (vsi.hasValueSet()) {
1710      if (isValueSetUnionImports()) {
1711        ok = false;
1712        for (UriType uri : vsi.getValueSet()) {
1713          if (inImport(path, getCu().pinValueSet(uri.getValue(), expansionParameters), system, version, code, info)) {
1714            return true;
1715          }
1716        }
1717      } else {
1718        Boolean bok = inImport(path, getCu().pinValueSet(vsi.getValueSet().get(0).getValue(), expansionParameters), system, version, code, info);
1719        if (bok == null) {
1720          return bok;
1721        }
1722        ok = bok;
1723        for (int i = 1; i < vsi.getValueSet().size(); i++) {
1724          UriType uri = vsi.getValueSet().get(i);
1725          ok = ok && inImport(path, getCu().pinValueSet(uri.getValue(), expansionParameters), system, version, code, info); 
1726        }
1727      }
1728    }
1729
1730    if (!vsi.hasSystem() || !ok) {
1731      return ok;
1732    }
1733    
1734    if (only && system == null) {
1735      // whether we know the system or not, we'll accept the stated codes at face value
1736      for (ConceptReferenceComponent cc : vsi.getConcept()) {
1737        if (cc.getCode().equals(code)) {
1738          return true;
1739        }
1740      }
1741    }
1742
1743    if (system == null || !system.equals(vsi.getSystem()))
1744      return false;
1745    // ok, we need the code system
1746    CodeSystem csa = context.fetchCodeSystem(system); //
1747    VersionAlgorithm va = csa == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(csa.getVersionAlgorithm());
1748    String actualVersion = determineVersion(path, system, vsi.getVersion(), version, issues, va);
1749    CodeSystem cs = resolveCodeSystem(system, actualVersion, valueset);
1750    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
1751      if (throwToServer) {
1752        // make up a transient value set with
1753        ValueSet vs = new ValueSet();
1754        vs.setStatus(PublicationStatus.ACTIVE);
1755        vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
1756        vs.setVersion(valueset.getVersion());
1757        vs.getCompose().addInclude(vsi);
1758        opContext.deadCheck("hit server "+vs.getVersionedUrl());
1759        ValidationResult res = context.validateCode(options.withNoClient(), new Coding(system, code, null), vs);
1760        if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
1761          if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
1762            // server didn't know the code system either - we'll take it face value
1763            if (!info.hasNotFound(system)) {
1764              String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM, system);
1765              info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.UNKNOWN, path, msg, OpIssueCode.NotFound, null, I18nConstants.UNKNOWN_CODESYSTEM));
1766              for (ConceptReferenceComponent cc : vsi.getConcept()) {
1767                if (cc.getCode().equals(code)) {
1768                  opContext.deadCheck("server true");
1769                  return true;
1770                }
1771              }
1772            }
1773            info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
1774            opContext.deadCheck("server codesystem unsupported");
1775            return null;
1776          }
1777          opContext.deadCheck("server not found");
1778          return false;
1779        }
1780        if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
1781          opContext.deadCheck("server no server");
1782          throw new NoTerminologyServiceException();
1783        }
1784        return res.isOk();
1785      } else {
1786        info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
1787        if (unknownSystems != null) {
1788          if (actualVersion == null) {
1789            unknownSystems.add(system);
1790          } else {
1791             unknownSystems.add(system+"|"+actualVersion);
1792          }
1793        }
1794        Set<String> versions = resolveCodeSystemVersions(system);
1795        if (!versions.isEmpty()) {
1796          String msg = context.formatMessage(I18nConstants.UNKNOWN_CODESYSTEM_VERSION, system, actualVersion, CommaSeparatedStringBuilder.join(",", Utilities.sorted(versions)));
1797          if (!hasIssue(issues, msg)) {
1798            issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, Utilities.noString(path) ? "version" : path + "." + "system", msg, OpIssueCode.NotFound, null, I18nConstants.UNKNOWN_CODESYSTEM_VERSION));
1799          }
1800        }
1801        return null;
1802      }
1803    } else {
1804      info.setFoundVersion(cs.getVersion());
1805      checkVersion(path, vsi.getSystem(), cs.getVersion(), va, issues);
1806      checkCanonical(info.getIssues(), path, cs, valueset);
1807      if ((valueset.getCompose().hasInactive() && !valueset.getCompose().getInactive()) || options.isActiveOnly()) {
1808        if (CodeSystemUtilities.isInactive(cs, code)) {
1809          info.addIssue(makeIssue(IssueSeverity.ERROR, IssueType.BUSINESSRULE, path+".code", context.formatMessage(I18nConstants.STATUS_CODE_WARNING_CODE, "not active", code), OpIssueCode.CodeRule, null, I18nConstants.STATUS_CODE_WARNING_CODE));
1810          return false;
1811        }
1812      }
1813      
1814      if (vsi.hasFilter()) {
1815        ok = true;
1816        int i = 0;
1817        for (ConceptSetFilterComponent f : vsi.getFilter()) {
1818          if (!codeInFilter(cs, vspath+".filter["+i+"]", system, f, code)) {
1819            return false;
1820          }
1821          i++;
1822        }
1823      }
1824
1825      List<ConceptDefinitionComponent> list = cs.getConcept();
1826      ok = validateCodeInConceptList(code, cs, list, allAltCodes);
1827      if (ok && vsi.hasConcept()) {
1828        for (ConceptReferenceComponent cc : vsi.getConcept()) {
1829          if (cc.getCode().equals(code)) {
1830            String sstatus = cc.getExtensionString(ExtensionDefinitions.EXT_STANDARDS_STATUS);
1831            if (Utilities.existsInList(sstatus, "withdrawn", "deprecated")) {
1832              String msg = context.formatMessage(I18nConstants.CONCEPT_DEPRECATED_IN_VALUESET, cs.getUrl(), cc.getCode(), sstatus, valueset.getVersionedUrl());
1833              info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.BUSINESSRULE, path+".code", msg, OpIssueCode.CodeComment, null, I18nConstants.CONCEPT_DEPRECATED_IN_VALUESET));
1834            } else if (cc.hasExtension(ExtensionDefinitions.EXT_VALUESET_DEPRECATED)) {
1835              String msg = context.formatMessage(I18nConstants.CONCEPT_DEPRECATED_IN_VALUESET, cs.getUrl(), cc.getCode(), "deprecated", valueset.getVersionedUrl());
1836              info.addIssue(makeIssue(IssueSeverity.WARNING, IssueType.BUSINESSRULE, path+".code", msg, OpIssueCode.CodeComment, null, I18nConstants.CONCEPT_DEPRECATED_IN_VALUESET));
1837            }
1838            return true;
1839          }
1840        }
1841        return false;
1842      } else {
1843        // recheck that this is a valid alternate code
1844        ok = validateCodeInConceptList(code, cs, list, altCodeParams);
1845        return ok;
1846      }
1847    }
1848  }
1849
1850  protected boolean isValueSetUnionImports() {
1851    PackageInformation p = (PackageInformation) valueset.getSourcePackage();
1852    if (p != null) {
1853      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
1854    } else {
1855      return false;
1856    }
1857  }
1858
1859  private boolean codeInFilter(CodeSystem cs, String path, String system, ConceptSetFilterComponent f, String code) throws FHIRException {
1860    String v = f.getValue();
1861    if (v == null) {
1862      List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
1863      issues.addAll(makeIssue(IssueSeverity.ERROR, IssueType.INVALID, path+".value", context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE,
1864        cs.getUrl(), f.getProperty(), f.getOp().toCode()), OpIssueCode.VSProcessing, null, I18nConstants.UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE));
1865      throw new VSCheckerException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE, cs.getUrl(), f.getProperty(), f.getOp().toCode()), issues, TerminologyServiceErrorClass.INTERNAL_ERROR);
1866      
1867    }
1868    if ("concept".equals(f.getProperty()))
1869      return codeInConceptFilter(cs, f, code);
1870    else if ("code".equals(f.getProperty()) && f.getOp() == FilterOperator.REGEX)
1871      return codeInRegexFilter(cs, f, code);
1872    else if (CodeSystemUtilities.isDefinedProperty(cs, f.getProperty())) {
1873      return codeInPropertyFilter(cs, f, code);
1874    } else if (isKnownProperty(f.getProperty())) {
1875      return codeInKnownPropertyFilter(cs, f, code);
1876    } else {
1877      log.error("todo: handle filters with property = "+f.getProperty()+" "+f.getOp().toCode());
1878      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty(), f.getOp().toCode()));
1879    }
1880  }
1881
1882  private boolean isKnownProperty(String code) {
1883    return Utilities.existsInList(code, "notSelectable");
1884  }
1885
1886  private boolean codeInPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1887    switch (f.getOp()) {
1888    case EQUAL:
1889      if (f.getValue() == null) {
1890        return false;
1891      }
1892      DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1893      return d != null && f.getValue().equals(d.primitiveValue());
1894    case EXISTS: 
1895      return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null;
1896    case REGEX:
1897      if (f.getValue() == null) {
1898        return false;
1899      }
1900      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1901      return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue());
1902    case IN:
1903      if (f.getValue() == null) {
1904        return false;
1905      }
1906      String[] values = f.getValue().split("\\,");
1907      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1908      if (d != null) {
1909        String v = d.primitiveValue();
1910        for (String value : values) {
1911          if (v != null && v.equals(value.trim())) {
1912            return true;
1913          }
1914        }
1915      }
1916      return false;
1917    case NOTIN:
1918      if (f.getValue() == null) {
1919        return true;
1920      }
1921      values = f.getValue().split("\\,");
1922      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1923      if (d != null) {
1924        String v = d.primitiveValue();
1925        for (String value : values) {
1926          if (v != null && v.equals(value.trim())) {
1927            return false;
1928          }
1929        }
1930      }
1931      return true;
1932    default:
1933      log.error("todo: handle property filters with op = "+f.getOp());
1934      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1935    }
1936  }
1937  
1938  private boolean codeInKnownPropertyFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1939
1940    switch (f.getOp()) {
1941    case EQUAL:
1942      if (f.getValue() == null) {
1943        return false;
1944      }
1945      DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1946      return d != null && f.getValue().equals(d.primitiveValue());
1947    case EXISTS: 
1948      return CodeSystemUtilities.getProperty(cs, code, f.getProperty()) != null;
1949    case REGEX:
1950      if (f.getValue() == null) {
1951        return false;
1952      }
1953      d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1954      return d != null && d.primitiveValue() != null && d.primitiveValue().matches(f.getValue());
1955    default:
1956      log.error("todo: handle known property filters with op = "+f.getOp());
1957      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1958    }
1959  }
1960
1961  private boolean codeInRegexFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
1962    return code.matches(f.getValue());
1963  }
1964
1965  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
1966    switch (f.getOp()) {
1967    case ISA: return codeInConceptIsAFilter(cs, f, code, false);
1968    case EQUAL:
1969      if (f.getValue() == null) {
1970        return false;
1971      }
1972      DataType d = CodeSystemUtilities.getProperty(cs, code, f.getProperty());
1973      return d != null && f.getValue().equals(d.primitiveValue());
1974    case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false);
1975    case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 
1976    default:
1977      log.error("todo: handle concept filters with op = "+f.getOp());
1978      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
1979    }
1980  }
1981
1982  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean excludeRoot) {
1983    if (!excludeRoot && code.equals(f.getValue())) {
1984      return true;
1985    }
1986    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue(), cs.getCaseSensitive(), altCodeParams);
1987    if (cc == null) {
1988      return false;
1989    }
1990    ConceptDefinitionComponent cc2 = findCodeInConcept(cc, code, cs.getCaseSensitive(), altCodeParams);
1991    return cc2 != null && cc2 != cc;
1992  }
1993
1994  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list, AlternateCodesProcessingRules altCodeRules) {
1995    opContext.deadCheck("validateCodeInConceptList");
1996    if (def.hasUserData(UserDataNames.tx_cs_special)) {
1997      return ((SpecialCodeSystem) def.getUserData(UserDataNames.tx_cs_special)).findConcept(new Coding().setCode(code)) != null; 
1998    } else if (def.getCaseSensitive()) {
1999      for (ConceptDefinitionComponent cc : list) {
2000        if (cc.getCode().equals(code)) { 
2001          return true;
2002        }
2003        if (Utilities.existsInList(code, alternateCodes(cc, altCodeRules))) {
2004          return true;
2005        }
2006        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) {
2007          return true;
2008        }
2009      }
2010    } else {
2011      for (ConceptDefinitionComponent cc : list) {
2012        if (cc.getCode().equalsIgnoreCase(code)) { 
2013          return true;
2014        }
2015        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept(), altCodeRules)) {
2016          return true;
2017        }
2018      }
2019    }
2020    return false;
2021  }
2022
2023  private ValueSetValidator getVs(String url, ValidationProcessInfo info) {
2024    if (inner.containsKey(url)) {
2025      return inner.get(url);
2026    }
2027    ValueSet vs = context.findTxResource(ValueSet.class, url, null, valueset);
2028    if (vs == null && info != null) {
2029      unknownValueSets.add(url);
2030      info.addIssue(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, null, context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_VALUE_SET_, url), OpIssueCode.NotFound, null, I18nConstants.UNABLE_TO_RESOLVE_VALUE_SET_));
2031    }
2032    ValueSetValidator vsc = new ValueSetValidator(context, opContext.copy(), options, vs, localContext, expansionParameters, tcm, registry);
2033    vsc.setThrowToServer(throwToServer);
2034    inner.put(url, vsc);
2035    return vsc;
2036  }
2037  
2038  private Boolean inImport(String path, String uri, String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
2039    ValueSetValidator vs = getVs(uri, info);
2040    if (vs == null) {
2041      return false;
2042    } else {
2043      Boolean ok = vs.codeInValueSet(path, system, version, code, info);
2044      return ok;
2045    }
2046  }
2047
2048
2049  private String getPreferredDisplay(ConceptReferenceComponent cc) {
2050    if (!options.hasLanguages()) {
2051      return cc.getDisplay();
2052    }
2053    if (LanguageUtils.langsMatch(options.getLanguages(), valueset.getLanguage())) {
2054      return cc.getDisplay();
2055    }
2056    // if there's no language, we default to accepting the displays as (US) English
2057    if (valueset.getLanguage() == null && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
2058      return cc.getDisplay();
2059    }
2060    for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
2061      if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
2062        return d.getValue();
2063      }
2064    }
2065    for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
2066      if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
2067        return d.getValue();
2068      }
2069    }
2070    return cc.getDisplay();
2071  }
2072
2073
2074  private String getPreferredDisplay(ConceptDefinitionComponent cc, CodeSystem cs) {
2075    if (!options.hasLanguages()) {
2076      return cc.getDisplay();
2077    }
2078    if (cs != null && LanguageUtils.langsMatch(options.getLanguages(), cs.getLanguage())) {
2079      return cc.getDisplay();
2080    }
2081    // if there's no language, we default to accepting the displays as (US) English
2082    if ((cs == null || cs.getLanguage() == null) && (options.langSummary().contains("en") || options.langSummary().contains("en-US"))) {
2083      return cc.getDisplay();
2084    }
2085    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
2086      if (!d.hasUse() && LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
2087        return d.getValue();
2088      }
2089    }
2090    for (ConceptDefinitionDesignationComponent d : cc.getDesignation()) {
2091      if (LanguageUtils.langsMatch(options.getLanguages(), d.getLanguage())) {
2092        return d.getValue();
2093      }
2094    }
2095    return cc.getDisplay();
2096  }
2097
2098}