001package org.hl7.fhir.r5.terminologies;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009
010 * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012 * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015 * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029
030 */
031
032
033
034import java.util.ArrayList;
035import java.util.Calendar;
036import java.util.GregorianCalendar;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042
043import org.hl7.fhir.exceptions.FHIRException;
044import org.hl7.fhir.exceptions.NoTerminologyServiceException;
045import org.hl7.fhir.r5.context.IWorkerContext;
046import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
047import org.hl7.fhir.r5.model.CanonicalType;
048import org.hl7.fhir.r5.model.CodeSystem;
049import org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode;
050import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
051import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
052import org.hl7.fhir.r5.model.CodeableConcept;
053import org.hl7.fhir.r5.model.Coding;
054import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
055import org.hl7.fhir.r5.model.PackageInformation;
056import org.hl7.fhir.r5.model.UriType;
057import org.hl7.fhir.r5.model.ValueSet;
058import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
059import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
060import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
061import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
062import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
063import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
064import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
065import org.hl7.fhir.r5.utils.ToolingExtensions;
066import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
067import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
068import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
069import org.hl7.fhir.utilities.Utilities;
070import org.hl7.fhir.utilities.VersionUtilities;
071import org.hl7.fhir.utilities.i18n.I18nConstants;
072import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
073import org.hl7.fhir.utilities.validation.ValidationOptions;
074import org.hl7.fhir.utilities.validation.ValidationOptions.ValueSetMode;
075
076public class ValueSetCheckerSimple extends ValueSetWorker implements ValueSetChecker {
077
078  private ValueSet valueset;
079  private IWorkerContext context;
080  private Map<String, ValueSetCheckerSimple> inner = new HashMap<>();
081  private ValidationOptions options;
082  private ValidationContextCarrier localContext;
083  private List<CodeSystem> localSystems = new ArrayList<>();
084
085  public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) {
086    this.valueset = source;
087    this.context = context;
088    this.options = options;
089  }
090  
091  public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt) {
092    this.valueset = source;
093    this.context = context;
094    this.options = options;
095    this.localContext = ctxt;
096    analyseValueSet();
097  }
098
099  private void analyseValueSet() {
100    if (localContext != null) {
101      if (valueset != null) {
102        for (ConceptSetComponent i : valueset.getCompose().getInclude()) {
103          analyseComponent(i);
104        }
105        for (ConceptSetComponent i : valueset.getCompose().getExclude()) {
106          analyseComponent(i);
107        }
108      }
109    }
110  }
111
112  private void analyseComponent(ConceptSetComponent i) {
113    if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) {
114      String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM);
115      if (ref.startsWith("#")) {
116        String id = ref.substring(1);
117        for (ValidationContextResourceProxy t : localContext.getResources()) {
118          CodeSystem cs = (CodeSystem) t.loadContainedResource(id, CodeSystem.class);
119          if (cs != null) {
120            localSystems.add(cs);
121          }
122        }
123      } else {        
124        throw new Error("Not done yet #2: "+ref);
125      }
126    }    
127  }
128
129  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
130    // first, we validate the codings themselves
131    List<String> errors = new ArrayList<String>();
132    ValidationProcessInfo info = new ValidationProcessInfo();
133    if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) {
134      for (Coding c : code.getCoding()) {
135        if (!c.hasSystem()) {
136          info.getWarnings().add(context.formatMessage(I18nConstants.CODING_HAS_NO_SYSTEM__CANNOT_VALIDATE));
137        }
138        CodeSystem cs = resolveCodeSystem(c.getSystem(), c.getVersion());
139        ValidationResult res = null;
140        if (cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) {
141          res = context.validateCode(options.noClient(), c, null);
142        } else {
143          res = validateCode(c, cs);
144        }
145        if (!res.isOk()) {
146          errors.add(res.getMessage());
147        } else if (res.getMessage() != null) {
148          info.getWarnings().add(res.getMessage());
149        }
150      }
151    }
152    Coding foundCoding = null;
153    if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
154      Boolean result = false;
155      for (Coding c : code.getCoding()) {
156        Boolean ok = codeInValueSet(c.getSystem(), c.getVersion(), c.getCode(), info);
157        if (ok == null && result == false) {
158          result = null;
159        } else if (ok) {
160          result = true;
161          foundCoding = c;
162        }
163      }
164      if (result == null) {
165        info.getWarnings().add(0, context.formatMessage(I18nConstants.UNABLE_TO_CHECK_IF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl()));        
166      } else if (!result) {
167        errors.add(0, context.formatMessage(I18nConstants.NONE_OF_THE_PROVIDED_CODES_ARE_IN_THE_VALUE_SET_, valueset.getUrl()));
168      }
169    }
170    if (errors.size() > 0) {
171      return new ValidationResult(IssueSeverity.ERROR, errors.toString());
172    } else if (info.getWarnings().size() > 0) {
173      return new ValidationResult(IssueSeverity.WARNING, info.getWarnings().toString());
174    } else {
175      ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode());
176      cd.setDisplay(foundCoding.getDisplay());
177      return new ValidationResult(foundCoding.getSystem(), cd);
178    }
179  }
180
181  public CodeSystem resolveCodeSystem(String system, String version) {
182    for (CodeSystem t : localSystems) {
183      if (t.getUrl().equals(system) && versionsMatch(version, t.getVersion())) {
184        return t;
185      }
186    }
187    CodeSystem cs = context.fetchCodeSystem(system, version);
188    if (cs == null) {
189      cs = findSpecialCodeSystem(system, version);
190    }
191    return cs;
192  }
193
194  private boolean versionsMatch(String versionTest, String versionActual) {
195    return versionTest == null && VersionUtilities.versionsMatch(versionTest, versionActual);
196  }
197
198  public ValidationResult validateCode(Coding code) throws FHIRException {
199    String warningMessage = null;
200    // first, we validate the concept itself
201
202    ValidationResult res = null;
203    boolean inExpansion = false;
204    boolean inInclude = false;
205    String system = code.hasSystem() ? code.getSystem() : getValueSetSystemOrNull();
206    if (options.getValueSetMode() != ValueSetMode.CHECK_MEMERSHIP_ONLY) {
207      if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum)
208        List<String> problems = new ArrayList<>();
209        system = systemForCodeInValueSet(code.getCode(), problems);
210        if (system == null) {
211          if (problems.size() == 0) {
212            throw new Error("Unable to resolve systems but no reason why"); // this is an error in the java code
213          } else if (problems.size() == 1) {
214            return new ValidationResult(IssueSeverity.ERROR, problems.get(0));
215          } else {
216            return new ValidationResult(IssueSeverity.ERROR, problems.toString());
217          }
218        }
219      }
220      if (!code.hasSystem()) {
221        if (options.isGuessSystem() && system == null && Utilities.isAbsoluteUrl(code.getCode())) {
222          system = "urn:ietf:rfc:3986"; // this arises when using URIs bound to value sets
223        }
224        code.setSystem(system);
225      }
226      inExpansion = checkExpansion(code);
227      inInclude = checkInclude(code);
228      CodeSystem cs = resolveCodeSystem(system, code.getVersion());
229      if (cs == null) {
230        warningMessage = "Unable to resolve system "+system;
231        if (!inExpansion) {
232          if (valueset != null && valueset.hasExpansion()) {
233            return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_UNK_EXPANSION, valueset.getUrl(), code.getCode().toString(), code.getSystem()));
234          } else {
235            throw new FHIRException(warningMessage);
236          }
237        }
238      }
239      if (cs != null && cs.hasSupplements()) {
240        return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.CODESYSTEM_CS_NO_SUPPLEMENT, cs.getUrl()));        
241      }
242      if (cs!=null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
243        warningMessage = "Resolved system "+system+(cs.hasVersion() ? " (v"+cs.getVersion()+")" : "")+", but the definition is not complete";
244        if (!inExpansion && cs.getContent() != CodeSystemContentMode.FRAGMENT) { // we're going to give it a go if it's a fragment
245          throw new FHIRException(warningMessage);
246        }
247      }
248
249      if (cs != null /*&& (cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)*/) {
250        if (!(cs.getContent() == CodeSystemContentMode.COMPLETE || cs.getContent() == CodeSystemContentMode.FRAGMENT)) {
251          if (inInclude) {
252            ConceptReferenceComponent cc = findInInclude(code);
253            if (cc != null) {
254              // we'll take it on faith
255              res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(cc.getCode()).setDisplay(cc.getDisplay()));
256              res.setMessage("Resolved system "+system+", but the definition is not complete, so assuming value set include is correct");
257              return res;
258            }
259          }
260          // we can't validate that here. 
261          throw new FHIRException("Unable to evaluate based on empty code system");
262        }
263        res = validateCode(code, cs);
264      } else if (cs == null && valueset.hasExpansion() && inExpansion) {
265        // we just take the value set as face value then
266        res = new ValidationResult(system, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()));
267      } else {
268        // well, we didn't find a code system - try the expansion? 
269        // disabled waiting for discussion
270        throw new FHIRException("No try the server");
271      }
272    } else {
273      inExpansion = checkExpansion(code);
274      inInclude = checkInclude(code);
275    }
276
277    ValidationProcessInfo info = new ValidationProcessInfo();
278    
279    // then, if we have a value set, we check it's in the value set
280    if (valueset != null && options.getValueSetMode() != ValueSetMode.NO_MEMBERSHIP_CHECK) {
281      if ((res==null || res.isOk())) { 
282        Boolean ok = codeInValueSet(system, code.getVersion(), code.getCode(), info);
283        if (ok == null || !ok) {
284          if (res == null) {
285            res = new ValidationResult((IssueSeverity) null, null);
286          }
287          if (info.getErr() != null) {
288            res.setErrorClass(info.getErr());
289          }
290          if (ok == null) {
291            res.setMessage("Unable to check whether code is in value set "+valueset.getUrl()+": "+info.getWarnings()).setSeverity(IssueSeverity.WARNING);
292          } else if (!inExpansion && !inInclude) {
293            if (!info.getWarnings().isEmpty()) {
294              res.setMessage("Not in value set "+valueset.getUrl()+": "+info.getWarnings()).setSeverity(IssueSeverity.ERROR);              
295            } else {
296              res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR);
297            }
298          } else if (warningMessage!=null) {
299            res = new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.CODE_FOUND_IN_EXPANSION_HOWEVER_, warningMessage));
300          } else if (inExpansion) {
301            res.setMessage("Code found in expansion, however: " + res.getMessage());
302          } else if (inInclude) {
303            res.setMessage("Code found in include, however: " + res.getMessage());
304          }
305        }
306      }
307    }
308    return res;
309  }
310
311  private boolean checkInclude(Coding code) {
312    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
313      return false;
314    }
315    for (ConceptSetComponent inc : valueset.getCompose().getExclude()) {
316      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
317        for (ConceptReferenceComponent cc : inc.getConcept()) {
318          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
319            return false;
320          }
321        }
322      }
323    }
324    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
325      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
326        for (ConceptReferenceComponent cc : inc.getConcept()) {
327          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
328            return true;
329          }
330        }
331      }
332    }
333    return false;
334  }
335
336  private ConceptReferenceComponent findInInclude(Coding code) {
337    if (valueset == null || code.getSystem() == null || code.getCode() == null) {
338      return null;
339    }
340    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
341      if (inc.hasSystem() && inc.getSystem().equals(code.getSystem())) {
342        for (ConceptReferenceComponent cc : inc.getConcept()) {
343          if (cc.hasCode() && cc.getCode().equals(code.getCode())) {
344            return cc;
345          }
346        }
347      }
348    }
349    return null;
350  }
351
352  private CodeSystem findSpecialCodeSystem(String system, String version) {
353    if ("urn:ietf:rfc:3986".equals(system)) {
354      CodeSystem cs = new CodeSystem();
355      cs.setUrl(system);
356      cs.setUserData("tx.cs.special", new URICodeSystem());
357      cs.setContent(CodeSystemContentMode.COMPLETE);
358      return cs; 
359    }
360    return null;
361  }
362
363  private ValidationResult findCodeInExpansion(Coding code) {
364    if (valueset==null || !valueset.hasExpansion())
365      return null;
366    return findCodeInExpansion(code, valueset.getExpansion().getContains());
367  }
368
369  private ValidationResult findCodeInExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
370    for (ValueSetExpansionContainsComponent containsComponent: contains) {
371      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
372        ConceptDefinitionComponent ccd = new ConceptDefinitionComponent();
373        ccd.setCode(containsComponent.getCode());
374        ccd.setDisplay(containsComponent.getDisplay());
375        ValidationResult res = new ValidationResult(code.getSystem(), ccd);
376        return res;
377      }
378      if (containsComponent.hasContains()) {
379        ValidationResult res = findCodeInExpansion(code, containsComponent.getContains());
380        if (res != null) {
381          return res;
382        }
383      }
384    }
385    return null;
386  }
387
388  private boolean checkExpansion(Coding code) {
389    if (valueset==null || !valueset.hasExpansion()) {
390      return false;
391    }
392    return checkExpansion(code, valueset.getExpansion().getContains());
393  }
394
395  private boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
396    for (ValueSetExpansionContainsComponent containsComponent: contains) {
397      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) {
398        return true;
399      }
400      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains())) {
401        return true;
402      }
403    }
404    return false;
405  }
406
407  private ValidationResult validateCode(Coding code, CodeSystem cs) {
408    ConceptDefinitionComponent cc = cs.hasUserData("tx.cs.special") ? ((SpecialCodeSystem) cs.getUserData("tx.cs.special")).findConcept(code) : findCodeInConcept(cs.getConcept(), code.getCode());
409    if (cc == null) {
410      if (cs.getContent() == CodeSystemContentMode.FRAGMENT) {
411        return new ValidationResult(IssueSeverity.WARNING, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_FRAGMENT, gen(code), cs.getUrl()));        
412      } else {
413        return new ValidationResult(IssueSeverity.ERROR, context.formatMessage(I18nConstants.UNKNOWN_CODE__IN_, gen(code), cs.getUrl()));
414      }
415    }
416    if (code.getDisplay() == null) {
417      return new ValidationResult(code.getSystem(), cc);
418    }
419    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
420    if (cc.hasDisplay()) {
421      b.append(cc.getDisplay());
422      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay())) {
423        return new ValidationResult(code.getSystem(), cc);
424      }
425    }
426    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
427      b.append(ds.getValue());
428      if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
429        return new ValidationResult(code.getSystem(), cc);
430      }
431    }
432    // also check to see if the value set has another display
433    ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode());
434    if (vs != null && (vs.hasDisplay() ||vs.hasDesignation())) {
435      if (vs.hasDisplay()) {
436        b.append(vs.getDisplay());
437        if (code.getDisplay().equalsIgnoreCase(vs.getDisplay())) {
438          return new ValidationResult(code.getSystem(), cc);
439        }
440      }
441      for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) {
442        b.append(ds.getValue());
443        if (code.getDisplay().equalsIgnoreCase(ds.getValue())) {
444          return new ValidationResult(code.getSystem(), cc);
445        }
446      }
447    }
448    return new ValidationResult(IssueSeverity.WARNING, context.formatMessagePlural(b.count(), I18nConstants.DISPLAY_NAME_FOR__SHOULD_BE_ONE_OF__INSTEAD_OF, code.getSystem(), code.getCode(), b.toString(), code.getDisplay()), code.getSystem(), cc);
449  }
450
451  private ConceptReferenceComponent findValueSetRef(String system, String code) {
452    if (valueset == null)
453      return null;
454    // if it has an expansion
455    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
456      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
457        ConceptReferenceComponent cc = new ConceptReferenceComponent();
458        cc.setDisplay(exp.getDisplay());
459        cc.setDesignation(exp.getDesignation());
460        return cc;
461      }
462    }
463    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
464      if (system.equals(inc.getSystem())) {
465        for (ConceptReferenceComponent cc : inc.getConcept()) {
466          if (cc.getCode().equals(code)) {
467            return cc;
468          }
469        }
470      }
471      for (CanonicalType url : inc.getValueSet()) {
472        ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code);
473        if (cc != null) {
474          return cc;
475        }
476      }
477    }
478    return null;
479  }
480
481  private String gen(Coding code) {
482    if (code.hasSystem()) {
483      return code.getSystem()+"#"+code.getCode();
484    } else {
485      return null;
486    }
487  }
488
489
490  private String getValueSetSystemOrNull() throws FHIRException {
491    if (valueset == null) {
492      return null;
493    }
494    if (valueset.getCompose().getInclude().size() == 0) {
495      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0) {
496        return null;
497      } else {
498        String cs = valueset.getExpansion().getContains().get(0).getSystem();
499        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs)) {
500          return cs;
501        } else {
502          return null;
503        }
504      }
505    }
506    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
507      if (inc.hasValueSet()) {
508        return null;
509      }
510      if (!inc.hasSystem()) {
511        return null;
512      }
513    }
514    if (valueset.getCompose().getInclude().size() == 1) {
515      return valueset.getCompose().getInclude().get(0).getSystem();
516    }
517
518    return null;
519  }
520
521  /*
522   * Check that all system values within an expansion correspond to the specified system value
523   */
524  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
525    for (ValueSetExpansionContainsComponent contains : containsList) {
526      if (!contains.getSystem().equals(system) || (contains.hasContains() && !checkSystem(contains.getContains(), system))) {
527        return false;
528      }
529    }
530    return true;
531  }
532
533  private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code) {
534    if (code.equals(concept.getCode())) {
535      return concept;
536    }
537    ConceptDefinitionComponent cc = findCodeInConcept(concept.getConcept(), code);
538    if (cc != null) {
539      return cc;
540    }
541    if (concept.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) {
542      List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) concept.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK);
543      for (ConceptDefinitionComponent c : children) {
544        cc = findCodeInConcept(c, code);
545        if (cc != null) {
546          return cc;
547        }
548      }
549    }
550    return null;
551  }
552  
553  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
554    for (ConceptDefinitionComponent cc : concept) {
555      if (code.equals(cc.getCode())) {
556        return cc;
557      }
558      ConceptDefinitionComponent c = findCodeInConcept(cc, code);
559      if (c != null) {
560        return c;
561      }
562    }
563    return null;
564  }
565
566
567  private String systemForCodeInValueSet(String code, List<String> problems) {
568    Set<String> sys = new HashSet<>();
569    if (!scanForCodeInValueSet(code, sys, problems)) {
570      return null;
571    }
572    if (sys.size() != 1) {
573      problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_MULTIPLE_MATCHES, sys.toString()));
574      return null;
575    } else {
576      return sys.iterator().next();
577    }
578  }
579  
580  private boolean scanForCodeInValueSet(String code, Set<String> sys, List<String> problems) {
581    if (valueset.hasCompose()) {
582      //  ignore excludes - they can't make any difference
583      if (!valueset.getCompose().hasInclude() && !valueset.getExpansion().hasContains()) {
584        problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_NO_INCLUDES_OR_EXPANSION, valueset.getVersionedUrl()));
585      }
586
587      int i = 0;
588      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
589        if (vsi.hasValueSet()) {
590          for (CanonicalType u : vsi.getValueSet()) {
591            if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) {
592              return false;
593            }
594          }
595        } else if (!vsi.hasSystem()) { 
596          problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i));
597          return false;
598        }
599        if (vsi.hasSystem()) {
600          if (vsi.hasFilter()) {
601            problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_NO_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem()));
602            return false;
603          }
604          CodeSystem cs = resolveCodeSystem(vsi.getSystem(), vsi.getVersion());
605          if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE) {
606
607            if (vsi.hasConcept()) {
608              for (ConceptReferenceComponent cc : vsi.getConcept()) {
609                boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
610                if (match) {
611                  sys.add(vsi.getSystem());
612                }
613              }
614            } else {
615              ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
616              if (cc != null) {
617                sys.add(vsi.getSystem());
618              }
619            }
620          } else if (vsi.hasConcept()) {
621            for (ConceptReferenceComponent cc : vsi.getConcept()) {
622              boolean match = cc.getCode().equals(code);
623              if (match) {
624                sys.add(vsi.getSystem());
625              }
626            }
627          } else {
628            // we'll try to expand this one then 
629            ValueSetExpansionOutcome vse = context.expandVS(vsi, false, false);
630            if (vse.isOk()) {
631              if (!checkSystems(vse.getValueset().getExpansion().getContains(), code, sys, problems)) {
632                return false;
633              }
634            } else {
635              problems.add(context.formatMessage(I18nConstants.UNABLE_TO_RESOLVE_SYSTEM__VALUE_SET_HAS_INCLUDE_WITH_UNKNOWN_SYSTEM, valueset.getVersionedUrl(), i, vsi.getSystem(), vse.getAllErrors().toString()));              
636              return false;
637            }
638          }
639        }
640        i++;
641      }
642    } else if (valueset.hasExpansion()) {
643      // Retrieve a list of all systems associated with this code in the expansion
644      if (!checkSystems(valueset.getExpansion().getContains(), code, sys, problems)) {
645        return false;
646      }
647    }
648    return true;
649  }
650
651  private boolean checkForCodeInValueSet(String code, String uri, Set<String> sys, List<String> problems) {
652    ValueSetCheckerSimple vs = getVs(uri);
653    return vs.scanForCodeInValueSet(code, sys, problems);
654  }
655
656  /*
657   * Recursively go through all codes in the expansion and for any coding that matches the specified code, add the system for that coding
658   * to the passed list. 
659   */
660  private boolean checkSystems(List<ValueSetExpansionContainsComponent> contains, String code, Set<String> systems, List<String> problems) {
661    for (ValueSetExpansionContainsComponent c: contains) {
662      if (c.getCode().equals(code)) {
663        systems.add(c.getSystem());
664      }
665      if (c.hasContains())
666        checkSystems(c.getContains(), code, systems, problems);
667    }
668    return true;
669  }
670  
671  @Override
672  public Boolean codeInValueSet(String system, String version, String code, ValidationProcessInfo info) throws FHIRException {
673    if (valueset == null) {
674      return false;
675    }
676    Boolean result = false;
677      
678    if (valueset.hasExpansion()) {
679      return checkExpansion(new Coding(system, code, null));
680    } else if (valueset.hasCompose()) {
681      int i = 0;
682      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
683        Boolean ok = inComponent(vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info);
684        i++;
685        if (ok == null && result == false) {
686          result = null;
687        } else if (ok) {
688          result = true;
689          break;
690        }
691      }
692      i = valueset.getCompose().getInclude().size();
693      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
694        Boolean nok = inComponent(vsi, i, system, version, code, valueset.getCompose().getInclude().size() == 1, info);
695        i++;
696        if (nok == null && result == false) {
697          result = null;
698        } else if (nok != null && nok) {
699          result = false;
700        }
701      }
702    } 
703
704    return result;
705  }
706
707  private Boolean inComponent(ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException {
708    boolean ok = true;
709    
710    if (vsi.hasValueSet()) {
711      if (isValueSetUnionImports()) {
712        ok = false;
713        for (UriType uri : vsi.getValueSet()) {
714          if (inImport(uri.getValue(), system, version, code)) {
715            return true;
716          }
717        }
718      } else {
719        ok = inImport(vsi.getValueSet().get(0).getValue(), system, version, code);
720        for (int i = 1; i < vsi.getValueSet().size(); i++) {
721          UriType uri = vsi.getValueSet().get(i);
722          ok = ok && inImport(uri.getValue(), system, version, code); 
723        }
724      }
725    }
726
727    if (!vsi.hasSystem() || !ok) {
728      return ok;
729    }
730    
731    if (only && system == null) {
732      // whether we know the system or not, we'll accept the stated codes at face value
733      for (ConceptReferenceComponent cc : vsi.getConcept()) {
734        if (cc.getCode().equals(code)) {
735          return true;
736        }
737      }
738    }
739
740    if (!system.equals(vsi.getSystem()))
741      return false;
742    // ok, we need the code system
743    CodeSystem cs = resolveCodeSystem(system, version);
744    if (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT)) {
745      // make up a transient value set with
746      ValueSet vs = new ValueSet();
747      vs.setStatus(PublicationStatus.ACTIVE);
748      vs.setUrl(valueset.getUrl()+"--"+vsiIndex);
749      vs.setVersion(valueset.getVersion());
750      vs.getCompose().addInclude(vsi);
751      ValidationResult res = context.validateCode(options.noClient(), new Coding(system, code, null), vs);
752      if (res.getErrorClass() == TerminologyServiceErrorClass.UNKNOWN || res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNSUPPORTED) {
753        if (info != null && res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
754       // server didn't know the code system either - we'll take it face value
755          info.getWarnings().add(context.formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, system));
756          for (ConceptReferenceComponent cc : vsi.getConcept()) {
757            if (cc.getCode().equals(code)) {
758              return true;
759            }
760          }
761          info.setErr(TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED);
762        }
763        return null;
764      }
765      if (res.getErrorClass() == TerminologyServiceErrorClass.NOSERVICE) {
766        throw new NoTerminologyServiceException();
767      }
768      return res.isOk();
769    } else {
770      if (vsi.hasFilter()) {
771        ok = true;
772        for (ConceptSetFilterComponent f : vsi.getFilter()) {
773          if (!codeInFilter(cs, system, f, code)) {
774            return false;
775          }
776        }
777      }
778
779      List<ConceptDefinitionComponent> list = cs.getConcept();
780      ok = validateCodeInConceptList(code, cs, list);
781      if (ok && vsi.hasConcept()) {
782        for (ConceptReferenceComponent cc : vsi.getConcept()) {
783          if (cc.getCode().equals(code)) { 
784            return true;
785          }
786        }
787        return false;
788      } else {
789        return ok;
790      }
791    }
792  }
793
794  protected boolean isValueSetUnionImports() {
795    PackageInformation p = (PackageInformation) valueset.getSourcePackage();
796    if (p != null) {
797      return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime());
798    } else {
799      return false;
800    }
801  }
802
803  private boolean codeInFilter(CodeSystem cs, String system, ConceptSetFilterComponent f, String code) throws FHIRException {
804    if ("concept".equals(f.getProperty()))
805      return codeInConceptFilter(cs, f, code);
806    else {
807      System.out.println("todo: handle filters with property = "+f.getProperty()); 
808      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__FILTER_WITH_PROPERTY__, cs.getUrl(), f.getProperty()));
809    }
810  }
811
812  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
813    switch (f.getOp()) {
814    case ISA: return codeInConceptIsAFilter(cs, f, code, false);
815    case ISNOTA: return !codeInConceptIsAFilter(cs, f, code, false);
816    case DESCENDENTOF: return codeInConceptIsAFilter(cs, f, code, true); 
817    default:
818      System.out.println("todo: handle concept filters with op = "+f.getOp()); 
819      throw new FHIRException(context.formatMessage(I18nConstants.UNABLE_TO_HANDLE_SYSTEM__CONCEPT_FILTER_WITH_OP__, cs.getUrl(), f.getOp()));
820    }
821  }
822
823  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code, boolean rootOnly) {
824    if (!rootOnly && code.equals(f.getProperty())) {
825      return true;
826    }
827    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue());
828    if (cc == null) {
829      return false;
830    }
831    cc = findCodeInConcept(cc, code);
832    return cc != null;
833  }
834
835  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) {
836    if (def.getCaseSensitive()) {
837      for (ConceptDefinitionComponent cc : list) {
838        if (cc.getCode().equals(code)) { 
839          return true;
840        }
841        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) {
842          return true;
843        }
844      }
845    } else {
846      for (ConceptDefinitionComponent cc : list) {
847        if (cc.getCode().equalsIgnoreCase(code)) { 
848          return true;
849        }
850        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept())) {
851          return true;
852        }
853      }
854    }
855    return false;
856  }
857
858  private ValueSetCheckerSimple getVs(String url) {
859    if (inner.containsKey(url)) {
860      return inner.get(url);
861    }
862    ValueSet vs = context.fetchResource(ValueSet.class, url, valueset);
863    ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context, localContext);
864    inner.put(url, vsc);
865    return vsc;
866  }
867
868  private boolean inImport(String uri, String system, String version, String code) throws FHIRException {
869    ValueSetCheckerSimple vs = getVs(uri);
870    if (vs == null) {
871      return false;
872    } else {
873      Boolean ok = vs.codeInValueSet(system, version, code, null);
874      return ok != null && ok;
875    }
876  }
877
878}