001package org.hl7.fhir.validation.instance.type;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import org.hl7.fhir.r5.context.IWorkerContext;
007import org.hl7.fhir.r5.context.IWorkerContext.CodingValidationRequest;
008import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
009import org.hl7.fhir.r5.elementmodel.Element;
010import org.hl7.fhir.r5.model.Coding;
011import org.hl7.fhir.r5.model.Resource;
012import org.hl7.fhir.r5.model.ValueSet;
013import org.hl7.fhir.r5.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
014import org.hl7.fhir.r5.utils.XVerExtensionManager;
015import org.hl7.fhir.utilities.Utilities;
016import org.hl7.fhir.utilities.VersionUtilities;
017import org.hl7.fhir.utilities.i18n.I18nConstants;
018import org.hl7.fhir.utilities.validation.ValidationMessage;
019import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
020import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
021import org.hl7.fhir.utilities.validation.ValidationOptions;
022import org.hl7.fhir.validation.BaseValidator;
023import org.hl7.fhir.validation.TimeTracker;
024import org.hl7.fhir.validation.instance.InstanceValidator;
025import org.hl7.fhir.validation.instance.utils.NodeStack;
026
027public class ValueSetValidator extends BaseValidator {
028
029  public class VSCodingValidationRequest extends CodingValidationRequest {
030
031    private NodeStack stack;
032
033    public VSCodingValidationRequest(NodeStack stack, Coding code) {
034      super(code);
035      this.stack = stack;
036    }
037
038    public NodeStack getStack() {
039      return stack;
040    }
041    
042  }
043
044  private InstanceValidator parent;
045
046  public ValueSetValidator(IWorkerContext context, TimeTracker timeTracker, InstanceValidator parent, XVerExtensionManager xverManager, Coding jurisdiction) {
047    super(context, xverManager);
048    source = Source.InstanceValidator;
049    this.timeTracker = timeTracker;
050    this.parent = parent;
051    this.jurisdiction = jurisdiction;
052
053  }
054  
055  public boolean validateValueSet(List<ValidationMessage> errors, Element vs, NodeStack stack) {
056    boolean ok = true;
057    if (!VersionUtilities.isR2Ver(context.getVersion())) {
058      List<Element> composes = vs.getChildrenByName("compose");
059      int cc = 0;
060      for (Element compose : composes) {
061        ok = validateValueSetCompose(errors, compose, stack.push(compose, cc, null, null), vs.getNamedChildValue("url"), "retired".equals(vs.getNamedChildValue("url"))) & ok;
062        cc++;
063      }
064    }
065    if (!stack.isContained()) {
066      ok = checkShareableValueSet(errors, vs, stack) && ok;
067    }
068    return ok;
069  }
070
071  private boolean checkShareableValueSet(List<ValidationMessage> errors, Element vs, NodeStack stack) {
072    if (parent.isForPublication()) { 
073      if (isHL7(vs)) {
074        boolean ok = true;
075        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("url"), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "url") && ok;                      
076        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("version"), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "version") && ok;                      
077        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("title"), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "title") && ok;                      
078        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("name"), I18nConstants.VALUESET_SHAREABLE_EXTRA_MISSING_HL7, "name");                      
079        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("status"), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "status") && ok;                      
080        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("experimental"), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "experimental") && ok;                      
081        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("description"), I18nConstants.VALUESET_SHAREABLE_MISSING_HL7, "description") && ok;
082        return ok;
083      } else {
084        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("url"), I18nConstants.VALUESET_SHAREABLE_MISSING, "url");                      
085        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("version"), I18nConstants.VALUESET_SHAREABLE_MISSING, "version");                      
086        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("title"), I18nConstants.VALUESET_SHAREABLE_MISSING, "title");                      
087        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("name"), I18nConstants.VALUESET_SHAREABLE_EXTRA_MISSING, "name");                      
088        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("status"), I18nConstants.VALUESET_SHAREABLE_MISSING, "status");                      
089        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("experimental"), I18nConstants.VALUESET_SHAREABLE_MISSING, "experimental");                      
090        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, vs.line(), vs.col(), stack.getLiteralPath(), vs.hasChild("description"), I18nConstants.VALUESET_SHAREABLE_MISSING, "description");
091      }
092    }
093    return true;
094  }
095
096
097  private boolean validateValueSetCompose(List<ValidationMessage> errors, Element compose, NodeStack stack, String vsid, boolean retired) {
098    boolean ok = true;
099    List<Element> includes = compose.getChildrenByName("include");
100    int ci = 0;
101    for (Element include : includes) {
102      ok = validateValueSetInclude(errors, include, stack.push(include, ci, null, null), vsid, retired) && ok;
103      ci++;
104    }    
105    List<Element> excludes = compose.getChildrenByName("exclude");
106    int ce = 0;
107    for (Element exclude : excludes) {
108      ok = validateValueSetInclude(errors, exclude, stack.push(exclude, ce, null, null), vsid, retired) && ok;
109      ce++;
110    }    
111    return ok;
112  }
113  
114  private boolean validateValueSetInclude(List<ValidationMessage> errors, Element include, NodeStack stack, String vsid, boolean retired) {
115    boolean ok = true;
116    String system = include.getChildValue("system");
117    String version = include.getChildValue("version");
118    List<Element> valuesets = include.getChildrenByName("valueSet");
119    int i = 0;
120    for (Element ve : valuesets) {
121      String v = ve.getValue();
122      ValueSet vs = context.fetchResource(ValueSet.class, v);
123      if (vs == null) {
124        NodeStack ns = stack.push(ve, i, ve.getProperty().getDefinition(), ve.getProperty().getDefinition());
125
126        Resource rs = context.fetchResource(Resource.class, v);
127        if (rs != null) {
128          warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), false, I18nConstants.VALUESET_REFERENCE_INVALID_TYPE, v, rs.fhirType());                      
129        } else { 
130          // todo: it's possible, at this point, that the txx server knows the value set, but it's not in scope
131          // should we handle this case?
132          warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), false, I18nConstants.VALUESET_REFERENCE_UNKNOWN, v);            
133        }
134      }
135      i++;
136    }
137    if (valuesets.size() > 1) {
138      warning(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, stack.getLiteralPath(), false, I18nConstants.VALUESET_IMPORT_UNION_INTERSECTION);                  
139    }
140    List<Element> concepts = include.getChildrenByName("concept");
141    List<Element> filters = include.getChildrenByName("filter");
142    if (!Utilities.noString(system)) {
143      boolean systemOk = true;
144      int cc = 0;
145      List<VSCodingValidationRequest> batch = new ArrayList<>();
146      boolean first = true;
147      for (Element concept : concepts) {
148        // we treat the first differently because we want to know if tbe system is worth validating. if it is, then we batch the rest
149        if (first) {
150          systemOk = validateValueSetIncludeConcept(errors, concept, stack, stack.push(concept, cc, null, null), system, version);
151          first = false;
152        } else if (systemOk) {
153          batch.add(prepareValidateValueSetIncludeConcept(errors, concept, stack.push(concept, cc, null, null), system, version));
154        }
155        cc++;
156      }    
157      if (parent.isValidateValueSetCodesOnTxServer() && batch.size() > 0) {
158        long t = System.currentTimeMillis();
159        if (parent.isDebug()) {
160          System.out.println("  : Validate "+batch.size()+" codes from "+system+" for "+vsid);
161        }
162        context.validateCodeBatch(ValidationOptions.defaults(), batch, null);
163        if (parent.isDebug()) {
164          System.out.println("  :   .. "+(System.currentTimeMillis()-t)+"ms");
165        }
166        for (VSCodingValidationRequest cv : batch) {
167          if (version == null) {
168            ok = warningOrHint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, cv.getStack().getLiteralPath(), cv.getResult().isOk(), !retired, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE, system, cv.getCoding().getCode()) && ok;
169          } else {
170            ok = warningOrHint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, cv.getStack().getLiteralPath(), cv.getResult().isOk(), !retired, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER, system, version, cv.getCoding().getCode()) && ok;
171          }
172        }
173      }
174      
175      int cf = 0;
176      for (Element filter : filters) {
177        if (systemOk && !validateValueSetIncludeFilter(errors, include, stack.push(filter, cf, null, null), system, version)) {
178          systemOk = false;          
179        }
180        cf++;
181      }    
182    } else {
183      warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), filters.size() == 0 && concepts.size() == 0, I18nConstants.VALUESET_NO_SYSTEM_WARNING);      
184    }
185    return ok;
186  }
187
188  private boolean validateValueSetIncludeConcept(List<ValidationMessage> errors, Element concept, NodeStack stackInc, NodeStack stack, String system, String version) {
189    String code = concept.getChildValue("code");
190    if (version == null) {
191      ValidationResult vv = context.validateCode(ValidationOptions.defaults(), new Coding(system, code, null), null);
192      if (vv.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
193        warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stackInc.getLiteralPath(), false, I18nConstants.VALUESET_UNC_SYSTEM_WARNING, system, vv.getMessage());                      
194        return false;
195      } else {
196        boolean ok = vv.isOk();
197        warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), ok, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE, system, code);
198      }
199    } else {
200      ValidationResult vv = context.validateCode(ValidationOptions.defaults(), new Coding(system, code, null).setVersion(version), null);
201      if (vv.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) {
202        warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stackInc.getLiteralPath(), false, I18nConstants.VALUESET_UNC_SYSTEM_WARNING_VER, system+"#"+version, vv.getMessage());            
203        return false;        
204      } else {
205        boolean ok = vv.isOk();
206        warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), ok, I18nConstants.VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER, system, version, code);
207      }
208    }
209    return true;
210  }
211
212  private VSCodingValidationRequest prepareValidateValueSetIncludeConcept(List<ValidationMessage> errors, Element concept, NodeStack stack, String system, String version) {
213    String code = concept.getChildValue("code");
214    Coding c = new Coding(system, code, null);
215    if (version != null) {
216       c.setVersion(version);
217    }
218    return new VSCodingValidationRequest(stack, c);
219  }
220
221  private boolean validateValueSetIncludeFilter(List<ValidationMessage> errors, Element include, NodeStack push, String system, String version) {
222    return true;
223  }
224}