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