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