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