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