001package org.hl7.fhir.r4.terminologies;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import org.hl7.fhir.exceptions.FHIRException;
007import org.hl7.fhir.r4.context.IWorkerContext;
008import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult;
009import org.hl7.fhir.r4.model.CodeSystem;
010import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
011import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
012import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent;
013import org.hl7.fhir.r4.model.CodeableConcept;
014import org.hl7.fhir.r4.model.Coding;
015import org.hl7.fhir.r4.model.UriType;
016import org.hl7.fhir.r4.model.ValueSet;
017import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
018import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
019import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent;
020import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
021import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
022import org.hl7.fhir.r4.utils.EOperationOutcome;
023import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
024import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
025
026public class ValueSetCheckerSimple implements ValueSetChecker {
027
028  private ValueSet valueset;
029  private IWorkerContext context;
030
031  public ValueSetCheckerSimple(ValueSet source, IWorkerContext context) {
032    this.valueset = source;
033    this.context = context;
034  }
035
036  public ValidationResult validateCode(CodeableConcept code) throws FHIRException {
037    // first, we validate the codings themselves
038    List<String> errors = new ArrayList<String>();
039    List<String> warnings = new ArrayList<String>();
040    for (Coding c : code.getCoding()) {
041      if (!c.hasSystem())
042        warnings.add("Coding has no system");
043      CodeSystem cs = context.fetchCodeSystem(c.getSystem());
044      if (cs == null)
045        throw new FHIRException("Unsupported system "+c.getSystem()+" - system is not specified or implicit");
046      if (cs.getContent() != CodeSystemContentMode.COMPLETE)
047        throw new FHIRException("Unable to resolve system "+c.getSystem()+" - system is not complete");
048      ValidationResult res = validateCode(c, cs);
049      if (!res.isOk())
050        errors.add(res.getMessage());
051      else if (res.getMessage() != null)
052        warnings.add(res.getMessage());
053    }
054    if (valueset != null) {
055      boolean ok = false;
056      for (Coding c : code.getCoding()) {
057        ok = ok || codeInValueSet(c.getSystem(), c.getCode());
058      }
059      if (!ok)
060        errors.add(0, "None of the provided codes are in the value set "+valueset.getUrl());
061    }
062    if (errors.size() > 0)
063      return new ValidationResult(IssueSeverity.ERROR, errors.toString());
064    else if (warnings.size() > 0)
065      return new ValidationResult(IssueSeverity.WARNING, warnings.toString());
066    else 
067      return new ValidationResult(IssueSeverity.INFORMATION, null);
068  }
069
070  public ValidationResult validateCode(Coding code) throws FHIRException {
071    // first, we validate the concept itself
072    String system = code.hasSystem() ? code.getSystem() : getValueSetSystem();
073    if (system == null && !code.hasDisplay()) { // dealing with just a plain code (enum)
074      system = systemForCodeInValueSet(code.getCode());
075    }
076    CodeSystem cs = context.fetchCodeSystem(system);
077    if (cs == null)
078      throw new FHIRException("Unable to resolve system "+system+" - system is not specified or implicit");
079    if (cs.getContent() != CodeSystemContentMode.COMPLETE)
080      throw new FHIRException("Unable to resolve system "+system+" - system is not complete");
081    ValidationResult res = validateCode(code, cs);
082      
083    // then, if we have a value set, we check it's in the value set
084    if (res.isOk() && valueset != null && !codeInValueSet(system, code.getCode()))
085      res.setMessage("Not in value set "+valueset.getUrl()).setSeverity(IssueSeverity.ERROR); 
086    return res;
087  }
088
089
090  private ValidationResult validateCode(Coding code, CodeSystem cs) {
091    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code.getCode());
092    if (cc == null)
093      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl());
094    if (code.getDisplay() == null)
095      return new ValidationResult(cc);
096    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
097    if (cc.hasDisplay()) {
098      b.append(cc.getDisplay());
099      if (code.getDisplay().equalsIgnoreCase(cc.getDisplay()))
100        return new ValidationResult(cc);
101    }
102    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
103      b.append(ds.getValue());
104      if (code.getDisplay().equalsIgnoreCase(ds.getValue()))
105        return new ValidationResult(cc);
106    }
107    return new ValidationResult(IssueSeverity.WARNING, "Display Name for "+code+" must be one of '"+b.toString()+"'", cc);
108  }
109
110  private String getValueSetSystem() throws FHIRException {
111    if (valueset == null)
112      throw new FHIRException("Unable to resolve system - no value set");
113    if (valueset.getCompose().hasExclude())
114      throw new FHIRException("Unable to resolve system - value set has excludes");
115    if (valueset.getCompose().getInclude().size() == 0)
116      throw new FHIRException("Unable to resolve system - value set has no includes");
117    for (ConceptSetComponent inc : valueset.getCompose().getInclude()) {
118      if (inc.hasValueSet())
119        throw new FHIRException("Unable to resolve system - value set has imports");
120      if (!inc.hasSystem())
121        throw new FHIRException("Unable to resolve system - value set has include with no system");
122    }
123    if (valueset.getCompose().getInclude().size() == 1)
124      return valueset.getCompose().getInclude().get(0).getSystem();
125    
126    return null;
127  }
128
129  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
130    for (ConceptDefinitionComponent cc : concept) {
131      if (code.equals(cc.getCode()))
132        return cc;
133      ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code);
134      if (c != null)
135        return c;
136    }
137    return null;
138  }
139
140  
141  private String systemForCodeInValueSet(String code) {
142    String sys = null;
143    if (valueset.hasCompose()) {
144      if (valueset.getCompose().hasExclude())
145        return null;
146      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
147        if (vsi.hasValueSet())
148          return null;
149        if (!vsi.hasSystem()) 
150          return null;
151        if (vsi.hasFilter())
152          return null;
153        CodeSystem cs = context.fetchCodeSystem(vsi.getSystem());
154        if (cs == null)
155          return null;
156        if (vsi.hasConcept()) {
157          for (ConceptReferenceComponent cc : vsi.getConcept()) {
158            boolean match = cs.getCaseSensitive() ? cc.getCode().equals(code) : cc.getCode().equalsIgnoreCase(code);
159            if (match) {
160              if (sys == null)
161                sys = vsi.getSystem();
162              else if (!sys.equals(vsi.getSystem()))
163                return null;
164            }
165          }
166        } else {
167          ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
168          if (cc != null) {
169            if (sys == null)
170              sys = vsi.getSystem();
171            else if (!sys.equals(vsi.getSystem()))
172              return null;
173          }
174        }
175      }
176    }
177    
178    return sys;  
179  }
180  
181  @Override
182  public boolean codeInValueSet(String system, String code) throws FHIRException {
183    if (valueset.hasCompose()) {
184      boolean ok = false;
185      for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) {
186        ok = ok || inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1);
187      }
188      for (ConceptSetComponent vsi : valueset.getCompose().getExclude()) {
189        ok = ok && !inComponent(vsi, system, code, valueset.getCompose().getInclude().size() == 1);
190      }
191      return ok;
192    }
193    
194    return false;
195  }
196
197  private boolean inComponent(ConceptSetComponent vsi, String system, String code, boolean only) throws FHIRException {
198    for (UriType uri : vsi.getValueSet()) {
199      if (inImport(uri.getValue(), system, code))
200        return true;
201    }
202
203    if (!vsi.hasSystem())
204      return false;
205    
206    if (only && system == null) {
207      // whether we know the system or not, we'll accept the stated codes at face value
208      for (ConceptReferenceComponent cc : vsi.getConcept())
209        if (cc.getCode().equals(code)) 
210          return true;
211    }
212    
213    if (!system.equals(vsi.getSystem()))
214      return false;
215    if (vsi.hasFilter())
216      throw new FHIRException("Filters - not done yet");
217    
218    CodeSystem def = context.fetchCodeSystem(system);
219    if (def.getContent() != CodeSystemContentMode.COMPLETE) 
220      throw new FHIRException("Unable to resolve system "+vsi.getSystem()+" - system is not complete");
221    
222    List<ConceptDefinitionComponent> list = def.getConcept();
223    return validateCodeInConceptList(code, def, list);
224  }
225
226  public boolean validateCodeInConceptList(String code, CodeSystem def, List<ConceptDefinitionComponent> list) {
227    if (def.getCaseSensitive()) {
228      for (ConceptDefinitionComponent cc : list) {
229        if (cc.getCode().equals(code)) 
230          return true;
231        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept()))
232          return true;
233      }
234    } else {
235      for (ConceptDefinitionComponent cc : list) {
236        if (cc.getCode().equalsIgnoreCase(code)) 
237          return true;
238        if (cc.hasConcept() && validateCodeInConceptList(code, def, cc.getConcept()))
239          return true;
240      }
241    }
242    return false;
243  }
244  
245  private boolean inImport(String uri, String system, String code) throws FHIRException {
246    ValueSet vs = context.fetchResource(ValueSet.class, uri);
247    ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(vs, context);
248    return vsc.codeInValueSet(system, code);
249  }
250
251}