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