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