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 org.hl7.fhir.exceptions.FHIRException;
038import org.hl7.fhir.r4.context.IWorkerContext;
039import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult;
040import org.hl7.fhir.r4.model.CanonicalType;
041import org.hl7.fhir.r4.model.CodeSystem;
042import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
043import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
044import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent;
045import org.hl7.fhir.r4.model.CodeableConcept;
046import org.hl7.fhir.r4.model.Coding;
047import org.hl7.fhir.r4.model.UriType;
048import org.hl7.fhir.r4.model.ValueSet;
049import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
050import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceDesignationComponent;
051import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
052import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent;
053import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
054import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
055import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
056import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
057import org.hl7.fhir.utilities.validation.ValidationOptions;
058
059@MarkedToMoveToAdjunctPackage
060public class ValueSetCheckerSimple implements ValueSetChecker {
061
062  private ValueSet valueset;
063  private IWorkerContext context;
064  private Map<String, ValueSetCheckerSimple> inner = new HashMap<>();
065  private ValidationOptions options;
066
067  public ValueSetCheckerSimple(ValidationOptions options, ValueSet source, IWorkerContext context) {
068    this.valueset = source;
069    this.context = context;
070    this.options = options;
071  }
072
073  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
074    // first, we validate the codings themselves
075    List<String> errors = new ArrayList<String>();
076    List<String> warnings = new ArrayList<String>();
077    for (Coding c : code.getCoding()) {
078      if (!c.hasSystem())
079        warnings.add("Coding has no system");
080      CodeSystem cs = context.fetchCodeSystem(c.getSystem());
081      if (cs == null)
082        warnings.add("Unsupported system " + c.getSystem() + " - system is not specified or implicit");
083      else if (cs.getContent() != CodeSystemContentMode.COMPLETE)
084        warnings.add("Unable to resolve system " + c.getSystem() + " - system is not complete");
085      else {
086        ValidationResult res = validateCode(c, cs);
087        if (!res.isOk())
088          errors.add(res.getMessage());
089        else if (res.getMessage() != null)
090          warnings.add(res.getMessage());
091      }
092    }
093    if (valueset != null) {
094      boolean ok = false;
095      for (Coding c : code.getCoding()) {
096        ok = ok || codeInValueSet(c.getSystem(), c.getCode());
097      }
098      if (!ok)
099        errors.add(0, "None of the provided codes are in the value set " + valueset.getUrl());
100    }
101    if (errors.size() > 0)
102      return new ValidationResult(IssueSeverity.ERROR, errors.toString());
103    else if (warnings.size() > 0)
104      return new ValidationResult(IssueSeverity.WARNING, warnings.toString());
105    else
106      return new ValidationResult(IssueSeverity.INFORMATION, null);
107  }
108
109  public ValidationResult validateCode(Coding code) throws FHIRException {
110    String warningMessage = null;
111    // first, we validate the concept itself
112
113    String system = code.hasSystem() ? code.getSystem() : getValueSetSystem();
114    if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum)
115      system = systemForCodeInValueSet(code.getCode());
116    }
117    if (!code.hasSystem())
118      code.setSystem(system);
119    boolean inExpansion = checkExpansion(code);
120    CodeSystem cs = context.fetchCodeSystem(system);
121    if (cs == null) {
122      warningMessage = "Unable to resolve system " + system + " - system is not specified or implicit";
123      if (!inExpansion)
124        throw new FHIRException(warningMessage);
125    }
126    if (cs != null && cs.getContent() != CodeSystemContentMode.COMPLETE) {
127      warningMessage = "Unable to resolve system " + system + " - system is not complete";
128      if (!inExpansion)
129        throw new FHIRException(warningMessage);
130    }
131
132    ValidationResult res = null;
133    if (cs != null)
134      res = validateCode(code, cs);
135
136    // then, if we have a value set, we check it's in the value set
137    if ((res == null || res.isOk()) && valueset != null && !codeInValueSet(system, code.getCode())) {
138      if (!inExpansion)
139        res.setMessage("Not in value set " + valueset.getUrl()).setSeverity(IssueSeverity.ERROR);
140      else if (warningMessage != null)
141        res = new ValidationResult(IssueSeverity.WARNING, "Code found in expansion, however: " + warningMessage);
142      else
143        res.setMessage("Code found in expansion, however: " + res.getMessage());
144    }
145    return res;
146  }
147
148  boolean checkExpansion(Coding code) {
149    if (valueset == null || !valueset.hasExpansion())
150      return false;
151    return checkExpansion(code, valueset.getExpansion().getContains());
152  }
153
154  boolean checkExpansion(Coding code, List<ValueSetExpansionContainsComponent> contains) {
155    for (ValueSetExpansionContainsComponent containsComponent : contains) {
156      if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode()))
157        return true;
158      if (containsComponent.hasContains() && checkExpansion(code, containsComponent.getContains()))
159        return true;
160    }
161    return false;
162  }
163
164  private ValidationResult validateCode(Coding code, CodeSystem cs) {
165    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code.getCode());
166    if (cc == null)
167      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code " + gen(code) + " in " + cs.getUrl());
168    if (code.getDisplay() == null)
169      return new ValidationResult(cc);
170    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
171    if (cc.hasDisplay()) {
172      b.append(cc.getDisplay());
173      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay()))
174        return new ValidationResult(cc);
175    }
176    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
177      b.append(ds.getValue());
178      if (code.getDisplay().equalsIgnoreCase(ds.getValue()))
179        return new ValidationResult(cc);
180    }
181    // also check to see if the value set has another display
182    ConceptReferenceComponent vs = findValueSetRef(code.getSystem(), code.getCode());
183    if (vs != null && (vs.hasDisplay() || vs.hasDesignation())) {
184      if (vs.hasDisplay()) {
185        b.append(vs.getDisplay());
186        if (code.getDisplay().equalsIgnoreCase(vs.getDisplay()))
187          return new ValidationResult(cc);
188      }
189      for (ConceptReferenceDesignationComponent ds : vs.getDesignation()) {
190        b.append(ds.getValue());
191        if (code.getDisplay().equalsIgnoreCase(ds.getValue()))
192          return new ValidationResult(cc);
193      }
194    }
195    return new ValidationResult(IssueSeverity.WARNING, "Display Name for " + code.getSystem() + "#" + code.getCode()
196        + " should be one of '" + b.toString() + "' instead of " + code.getDisplay(), cc);
197  }
198
199  private ConceptReferenceComponent findValueSetRef(String system, String code) {
200    if (valueset == null)
201      return null;
202    // if it has an expansion
203    for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) {
204      if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) {
205        ConceptReferenceComponent cc = new ConceptReferenceComponent();
206        cc.setDisplay(exp.getDisplay());
207        cc.setDesignation(exp.getDesignation());
208        return cc;
209      }
210    }
211    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
212      if (system.equals(inc.getSystem())) {
213        for (ConceptReferenceComponent cc : inc.getConcept()) {
214          if (cc.getCode().equals(code))
215            return cc;
216        }
217      }
218      for (CanonicalType url : inc.getValueSet()) {
219        ConceptReferenceComponent cc = getVs(url.asStringValue()).findValueSetRef(system, code);
220        if (cc != null)
221          return cc;
222      }
223    }
224    return null;
225  }
226
227  private String gen(Coding code) {
228    if (code.hasSystem())
229      return code.getSystem() + "#" + code.getCode();
230    else
231      return null;
232  }
233
234  private String getValueSetSystem() throws FHIRException {
235    if (valueset == null)
236      throw new FHIRException("Unable to resolve system - no value set");
237    if (valueset.getCompose().getInclude().size() == 0) {
238      if (!valueset.hasExpansion() || valueset.getExpansion().getContains().size() == 0)
239        throw new FHIRException("Unable to resolve system - value set has no includes or expansion");
240      else {
241        String cs = valueset.getExpansion().getContains().get(0).getSystem();
242        if (cs != null && checkSystem(valueset.getExpansion().getContains(), cs))
243          return cs;
244        else
245          throw new FHIRException("Unable to resolve system - value set expansion has multiple systems");
246      }
247    }
248    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
249      if (inc.hasValueSet())
250        throw new FHIRException("Unable to resolve system - value set has imports");
251      if (!inc.hasSystem())
252        throw new FHIRException("Unable to resolve system - value set has include with no system");
253    }
254    if (valueset.getCompose().getInclude().size() == 1)
255      return valueset.getCompose().getInclude().get(0).getSystem();
256
257    return null;
258  }
259
260  /*
261   * Check that all system values within an expansion correspond to the specified
262   * system value
263   */
264  private boolean checkSystem(List<ValueSetExpansionContainsComponent> containsList, String system) {
265    for (ValueSetExpansionContainsComponent contains : containsList) {
266      if (!contains.getSystem().equals(system)
267          || (contains.hasContains() && !checkSystem(contains.getContains(), system)))
268        return false;
269    }
270    return true;
271  }
272
273  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
274    for (ConceptDefinitionComponent cc : concept) {
275      if (code.equals(cc.getCode()))
276        return cc;
277      ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code);
278      if (c != null)
279        return c;
280    }
281    return null;
282  }
283
284  private String systemForCodeInValueSet(String code) {
285    String sys = null;
286    if (valueset.hasCompose()) {
287      if (valueset.getCompose().hasExclude())
288        return null;
289      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
290        if (vsi.hasValueSet())
291          return null;
292        if (!vsi.hasSystem())
293          return null;
294        if (vsi.hasFilter())
295          return null;
296        CodeSystem cs = context.fetchCodeSystem(vsi.getSystem());
297        if (cs == null)
298          return null;
299        if (vsi.hasConcept()) {
300          for (ConceptReferenceComponent cc : vsi.getConcept()) {
301            boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
302            if (match) {
303              if (sys == null)
304                sys = vsi.getSystem();
305              else if (!sys.equals(vsi.getSystem()))
306                return null;
307            }
308          }
309        } else {
310          ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
311          if (cc != null) {
312            if (sys == null)
313              sys = vsi.getSystem();
314            else if (!sys.equals(vsi.getSystem()))
315              return null;
316          }
317        }
318      }
319    }
320
321    return sys;
322  }
323
324  @Override
325  public boolean codeInValueSet(String system, String code) throws FHIRException {
326    if (valueset.hasExpansion()) {
327      return checkExpansion(new Coding(system, code, null));
328    } else if (valueset.hasCompose()) {
329      boolean ok = false;
330      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
331        ok = ok || inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1);
332      }
333      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
334        ok = ok && !inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1);
335      }
336      return ok;
337    }
338
339    return false;
340  }
341
342  private boolean inComponent(ConceptSetComponent vsi, String system, String code, boolean only) throws FHIRException {
343    for (UriType uri : vsi.getValueSet()) {
344      if (inImport(uri.getValue(), system, code))
345        return true;
346    }
347
348    if (!vsi.hasSystem())
349      return false;
350
351    if (only && system == null) {
352      // whether we know the system or not, we'll accept the stated codes at face
353      // value
354      for (ConceptReferenceComponent cc : vsi.getConcept())
355        if (cc.getCode().equals(code))
356          return true;
357    }
358
359    if (!system.equals(vsi.getSystem()))
360      return false;
361    if (vsi.hasFilter()) {
362      boolean ok = true;
363      for (ConceptSetFilterComponent f : vsi.getFilter())
364        if (!codeInFilter(system, f, code)) {
365          ok = false;
366          break;
367        }
368      if (ok)
369        return true;
370    }
371
372    CodeSystem def = context.fetchCodeSystem(system);
373    if (def.getContent() != CodeSystemContentMode.COMPLETE)
374      throw new FHIRException("Unable to resolve system " + vsi.getSystem() + " - system is not complete");
375
376    List<ConceptDefinitionComponent> list = def.getConcept();
377    boolean ok = validateCodeInConceptList(code, def, list);
378    if (ok && vsi.hasConcept()) {
379      for (ConceptReferenceComponent cc : vsi.getConcept())
380        if (cc.getCode().equals(code))
381          return true;
382      return false;
383    } else
384      return ok;
385  }
386
387  private boolean codeInFilter(String system, ConceptSetFilterComponent f, String code) throws FHIRException {
388    CodeSystem cs = context.fetchCodeSystem(system);
389    if (cs == null)
390      throw new FHIRException("Unable to evaluate filters on unknown code system '" + system + "'");
391    if ("concept".equals(f.getProperty()))
392      return codeInConceptFilter(cs, f, code);
393    else {
394      System.out.println("todo: handle filters with property = " + f.getProperty());
395      throw new FHIRException("Unable to handle system " + cs.getUrl() + " filter with property = " + f.getProperty());
396    }
397  }
398
399  private boolean codeInConceptFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) throws FHIRException {
400    switch (f.getOp()) {
401    case ISA:
402      return codeInConceptIsAFilter(cs, f, code);
403    case ISNOTA:
404      return !codeInConceptIsAFilter(cs, f, code);
405    default:
406      System.out.println("todo: handle concept filters with op = " + f.getOp());
407      throw new FHIRException("Unable to handle system " + cs.getUrl() + " concept filter with op = " + f.getOp());
408    }
409  }
410
411  private boolean codeInConceptIsAFilter(CodeSystem cs, ConceptSetFilterComponent f, String code) {
412    if (code.equals(f.getProperty()))
413      return true;
414    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), f.getValue());
415    if (cc == null)
416      return false;
417    cc = findCodeInConcept(cc.getConcept(), code);
418    return cc != null;
419  }
420
421  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) {
422    if (def.getCaseSensitive()) {
423      for (ConceptDefinitionComponent cc : list) {
424        if (cc.getCode().equals(code))
425          return true;
426        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept()))
427          return true;
428      }
429    } else {
430      for (ConceptDefinitionComponent cc : list) {
431        if (cc.getCode().equalsIgnoreCase(code))
432          return true;
433        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept()))
434          return true;
435      }
436    }
437    return false;
438  }
439
440  private ValueSetCheckerSimple getVs(String url) {
441    if (inner.containsKey(url)) {
442      return inner.get(url);
443    }
444    ValueSet vs = context.fetchResource(ValueSet.class, url);
445    ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, context);
446    inner.put(url, vsc);
447    return vsc;
448  }
449
450  private boolean inImport(String uri, String system, String code) throws FHIRException {
451    return getVs(uri).codeInValueSet(system, code);
452  }
453
454}