001package org.hl7.fhir.r4.terminologies;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.List;
006import java.util.Map;
007
008/*
009  Copyright (c) 2011+, HL7, Inc.
010  All rights reserved.
011  
012  Redistribution and use in source and binary forms, with or without modification, 
013  are permitted provided that the following conditions are met:
014    
015   * Redistributions of source code must retain the above copyright notice, this 
016     list of conditions and the following disclaimer.
017   * Redistributions in binary form must reproduce the above copyright notice, 
018     this list of conditions and the following disclaimer in the documentation 
019     and/or other materials provided with the distribution.
020   * Neither the name of HL7 nor the names of its contributors may be used to 
021     endorse or promote products derived from this software without specific 
022     prior written permission.
023  
024  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
025  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
026  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
027  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
028  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
029  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
030  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
031  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
032  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
033  POSSIBILITY OF SUCH DAMAGE.
034  
035 */
036
037import lombok.extern.slf4j.Slf4j;
038import org.hl7.fhir.exceptions.FHIRException;
039import org.hl7.fhir.r4.context.IWorkerContext;
040import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult;
041import org.hl7.fhir.r4.model.CanonicalType;
042import org.hl7.fhir.r4.model.CodeSystem;
043import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
044import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
045import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent;
046import org.hl7.fhir.r4.model.CodeableConcept;
047import org.hl7.fhir.r4.model.Coding;
048import org.hl7.fhir.r4.model.UriType;
049import org.hl7.fhir.r4.model.ValueSet;
050import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
051import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceDesignationComponent;
052import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
053import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent;
054import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
055import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
056import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
057import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
058import org.hl7.fhir.utilities.validation.ValidationOptions;
059
060@MarkedToMoveToAdjunctPackage
061@Slf4j
062public class ValueSetCheckerSimple implements ValueSetChecker {
063
064  private ValueSet valueset;
065  private IWorkerContext context;
066  private Map<String, ValueSetCheckerSimple> inner = new HashMap<>();
067  private ValidationOptions options;
068
069  public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) {
070    this.valueset = source;
071    this.context = context;
072    this.options = options;
073  }
074
075  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
076    // first, we validate the codings themselves
077    List<String> errors = new ArrayList<String>();
078    List<String> warnings = new ArrayList<String>();
079    for (Coding c : code.getCoding()) {
080      if (!c.hasSystem())
081        warnings.add("Coding has no system");
082      CodeSystem cs = context.fetchCodeSystem(c.getSystem());
083      if (cs == null)
084        warnings.add("Unsupported system " + c.getSystem() + " - system is not specified or implicit");
085      else if (cs.getContent() != CodeSystemContentMode.COMPLETE)
086        warnings.add("Unable to resolve system " + c.getSystem() + " - system is not complete");
087      else {
088        ValidationResult res = validateCode(c, cs);
089        if (!res.isOk())
090          errors.add(res.getMessage());
091        else if (res.getMessage() != null)
092          warnings.add(res.getMessage());
093      }
094    }
095    if (valueset != null) {
096      boolean ok = false;
097      for (Coding c : code.getCoding()) {
098        ok = ok || codeInValueSet(c.getSystem(), c.getCode());
099      }
100      if (!ok)
101        errors.add(0, "None of the provided codes are in the value set " + valueset.getUrl());
102    }
103    if (errors.size() > 0)
104      return new ValidationResult(IssueSeverity.ERROR, errors.toString());
105    else if (warnings.size() > 0)
106      return new ValidationResult(IssueSeverity.WARNING, warnings.toString());
107    else
108      return new ValidationResult(IssueSeverity.INFORMATION, null);
109  }
110
111  public ValidationResult validateCode(Coding code) throws FHIRException {
112    String warningMessage = null;
113    // first, we validate the concept itself
114
115    String system = code.hasSystem() ? code.getSystem() : getValueSetSystem();
116    if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum)
117      system = systemForCodeInValueSet(code.getCode());
118    }
119    if (!code.hasSystem())
120      code.setSystem(system);
121    boolean inExpansion = checkExpansion(code);
122    CodeSystem cs = context.fetchCodeSystem(system);
123    if (cs == null) {
124      warningMessage = "Unable to resolve system " + system + " - system is not specified or implicit";
125      if (!inExpansion)
126        throw new FHIRException(warningMessage);
127    }
128    if (cs != null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
129      warningMessage = "Unable to resolve system " + system + " - system is not complete";
130      if (!inExpansion)
131        throw new FHIRException(warningMessage);
132    }
133
134    ValidationResult res = null;
135    if (cs != null)
136      res = validateCode(code, cs);
137
138    // then, if we have a value set, we check it's in the value set
139    if ((res == null || res.isOk()) && valueset != null && !codeInValueSet(system, code.getCode())) {
140      if (!inExpansion)
141        res.setMessage("Not in value set " + valueset.getUrl()).setSeverity(IssueSeverity.ERROR);
142      else if (warningMessage != null)
143        res = new ValidationResult(IssueSeverity.WARNING, "Code found in expansion, however: " + warningMessage);
144      else
145        res.setMessage("Code found in expansion, however: " + res.getMessage());
146    }
147    return res;
148  }
149
150  boolean checkExpansion(Coding code) {
151    if (valueset == null || !valueset.hasExpansion())
152      return false;
153    return checkExpansion(code, valueset.getExpansion().getContains());
154  }
155
156  boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
157    for (ValueSetExpansionContainsComponent containsComponent : contains) {
158      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode()))
159        return true;
160      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains()))
161        return true;
162    }
163    return false;
164  }
165
166  private ValidationResult validateCode(Coding code, CodeSystem cs) {
167    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code.getCode());
168    if (cc == null)
169      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code " + gen(code) + " in " + cs.getUrl());
170    if (code.getDisplay() == null)
171      return new ValidationResult(cc);
172    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
173    if (cc.hasDisplay()) {
174      b.append(cc.getDisplay());
175      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay()))
176        return new ValidationResult(cc);
177    }
178    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
179      b.append(ds.getValue());
180      if (code.getDisplay().equalsIgnoreCase(ds.getValue()))
181        return new ValidationResult(cc);
182    }
183    // also check to see if the value set has another display
184    ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode());
185    if (vs != null && (vs.hasDisplay() || vs.hasDesignation())) {
186      if (vs.hasDisplay()) {
187        b.append(vs.getDisplay());
188        if (code.getDisplay().equalsIgnoreCase(vs.getDisplay()))
189          return new ValidationResult(cc);
190      }
191      for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) {
192        b.append(ds.getValue());
193        if (code.getDisplay().equalsIgnoreCase(ds.getValue()))
194          return new ValidationResult(cc);
195      }
196    }
197    return new ValidationResult(IssueSeverity.WARNING, "Display Name for " + code.getSystem() + "#" + code.getCode()
198        + " should be one of '" + b.toString() + "' instead of " + code.getDisplay(), cc);
199  }
200
201  private ConceptReferenceComponent findValueSetRef(String system, String code) {
202    if (valueset == null)
203      return null;
204    // if it has an expansion
205    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
206      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
207        ConceptReferenceComponent cc = new ConceptReferenceComponent();
208        cc.setDisplay(exp.getDisplay());
209        cc.setDesignation(exp.getDesignation());
210        return cc;
211      }
212    }
213    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
214      if (system.equals(inc.getSystem())) {
215        for (ConceptReferenceComponent cc : inc.getConcept()) {
216          if (cc.getCode().equals(code))
217            return cc;
218        }
219      }
220      for (CanonicalType url : inc.getValueSet()) {
221        ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code);
222        if (cc != null)
223          return cc;
224      }
225    }
226    return null;
227  }
228
229  private String gen(Coding code) {
230    if (code.hasSystem())
231      return code.getSystem() + "#" + code.getCode();
232    else
233      return null;
234  }
235
236  private String getValueSetSystem() throws FHIRException {
237    if (valueset == null)
238      throw new FHIRException("Unable to resolve system - no value set");
239    if (valueset.getCompose().getInclude().size() == 0) {
240      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0)
241        throw new FHIRException("Unable to resolve system - value set has no includes or expansion");
242      else {
243        String cs = valueset.getExpansion().getContains().get(0).getSystem();
244        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs))
245          return cs;
246        else
247          throw new FHIRException("Unable to resolve system - value set expansion has multiple systems");
248      }
249    }
250    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
251      if (inc.hasValueSet())
252        throw new FHIRException("Unable to resolve system - value set has imports");
253      if (!inc.hasSystem())
254        throw new FHIRException("Unable to resolve system - value set has include with no system");
255    }
256    if (valueset.getCompose().getInclude().size() == 1)
257      return valueset.getCompose().getInclude().get(0).getSystem();
258
259    return null;
260  }
261
262  /*
263   * Check that all system values within an expansion correspond to the specified
264   * system value
265   */
266  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
267    for (ValueSetExpansionContainsComponent contains : containsList) {
268      if (!contains.getSystem().equals(system)
269          || (contains.hasContains() && !checkSystem(contains.getContains(), system)))
270        return false;
271    }
272    return true;
273  }
274
275  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
276    for (ConceptDefinitionComponent cc : concept) {
277      if (code.equals(cc.getCode()))
278        return cc;
279      ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code);
280      if (c != null)
281        return c;
282    }
283    return null;
284  }
285
286  private String systemForCodeInValueSet(String code) {
287    String sys = null;
288    if (valueset.hasCompose()) {
289      if (valueset.getCompose().hasExclude())
290        return null;
291      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
292        if (vsi.hasValueSet())
293          return null;
294        if (!vsi.hasSystem())
295          return null;
296        if (vsi.hasFilter())
297          return null;
298        CodeSystem cs = context.fetchCodeSystem(vsi.getSystem());
299        if (cs == null)
300          return null;
301        if (vsi.hasConcept()) {
302          for (ConceptReferenceComponent cc : vsi.getConcept()) {
303            boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
304            if (match) {
305              if (sys == null)
306                sys = vsi.getSystem();
307              else if (!sys.equals(vsi.getSystem()))
308                return null;
309            }
310          }
311        } else {
312          ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
313          if (cc != null) {
314            if (sys == null)
315              sys = vsi.getSystem();
316            else if (!sys.equals(vsi.getSystem()))
317              return null;
318          }
319        }
320      }
321    }
322
323    return sys;
324  }
325
326  @Override
327  public boolean codeInValueSet(String system, String code) throws FHIRException {
328    if (valueset.hasExpansion()) {
329      return checkExpansion(new Coding(system, code, null));
330    } else if (valueset.hasCompose()) {
331      boolean ok = false;
332      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
333        ok = ok || inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1);
334      }
335      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
336        ok = ok && !inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1);
337      }
338      return ok;
339    }
340
341    return false;
342  }
343
344  private boolean inComponent(ConceptSetComponent vsi, String system, String code, boolean only) throws FHIRException {
345    for (UriType uri : vsi.getValueSet()) {
346      if (inImport(uri.getValue(), system, code))
347        return true;
348    }
349
350    if (!vsi.hasSystem())
351      return false;
352
353    if (only && system == null) {
354      // whether we know the system or not, we'll accept the stated codes at face
355      // value
356      for (ConceptReferenceComponent cc : vsi.getConcept())
357        if (cc.getCode().equals(code))
358          return true;
359    }
360
361    if (!system.equals(vsi.getSystem()))
362      return false;
363    if (vsi.hasFilter()) {
364      boolean ok = true;
365      for (ConceptSetFilterComponent f : vsi.getFilter())
366        if (!codeInFilter(system, f, code)) {
367          ok = false;
368          break;
369        }
370      if (ok)
371        return true;
372    }
373
374    CodeSystem def = context.fetchCodeSystem(system);
375    if (def.getContent() != CodeSystemContentMode.COMPLETE)
376      throw new FHIRException("Unable to resolve system " + vsi.getSystem() + " - system is not complete");
377
378    List<ConceptDefinitionComponent> list = def.getConcept();
379    boolean ok = validateCodeInConceptList(code, def, list);
380    if (ok && vsi.hasConcept()) {
381      for (ConceptReferenceComponent cc : vsi.getConcept())
382        if (cc.getCode().equals(code))
383          return true;
384      return false;
385    } else
386      return ok;
387  }
388
389  private boolean codeInFilter(String system, ConceptSetFilterComponent f, String code) throws FHIRException {
390    CodeSystem cs = context.fetchCodeSystem(system);
391    if (cs == null)
392      throw new FHIRException("Unable to evaluate filters on unknown code system '" + system + "'");
393    if ("concept".equals(f.getProperty()))
394      return codeInConceptFilter(cs, f, code);
395    else {
396      log.error("todo: handle filters with property = " + f.getProperty());
397      throw new FHIRException("Unable to handle system " + cs.getUrl() + " filter with property = " + f.getProperty());
398    }
399  }
400
401  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
402    switch (f.getOp()) {
403    case ISA:
404      return codeInConceptIsAFilter(cs, f, code);
405    case ISNOTA:
406      return !codeInConceptIsAFilter(cs, f, code);
407    default:
408      log.error("todo: handle concept filters with op = " + f.getOp());
409      throw new FHIRException("Unable to handle system " + cs.getUrl() + " concept filter with op = " + f.getOp());
410    }
411  }
412
413  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
414    if (code.equals(f.getProperty()))
415      return true;
416    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue());
417    if (cc == null)
418      return false;
419    cc = findCodeInConcept(cc.getConcept(), code);
420    return cc != null;
421  }
422
423  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) {
424    if (def.getCaseSensitive()) {
425      for (ConceptDefinitionComponent cc : list) {
426        if (cc.getCode().equals(code))
427          return true;
428        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept()))
429          return true;
430      }
431    } else {
432      for (ConceptDefinitionComponent cc : list) {
433        if (cc.getCode().equalsIgnoreCase(code))
434          return true;
435        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept()))
436          return true;
437      }
438    }
439    return false;
440  }
441
442  private ValueSetCheckerSimple getVs(String url) {
443    if (inner.containsKey(url)) {
444      return inner.get(url);
445    }
446    ValueSet vs = context.fetchResource(ValueSet.class, url);
447    ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context);
448    inner.put(url, vsc);
449    return vsc;
450  }
451
452  private boolean inImport(String uri, String system, String code) throws FHIRException {
453    return getVs(uri).codeInValueSet(system, code);
454  }
455
456}