001package org.hl7.fhir.validation.instance.type;
002
003import static org.apache.commons.lang3.StringUtils.isNotBlank;
004
005import java.io.ByteArrayOutputStream;
006import java.io.IOException;
007import java.math.BigDecimal;
008import java.util.ArrayList;
009import java.util.List;
010
011import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
012import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
013import org.hl7.fhir.exceptions.FHIRException;
014import org.hl7.fhir.r5.context.IWorkerContext;
015import org.hl7.fhir.r5.elementmodel.Element;
016import org.hl7.fhir.r5.elementmodel.JsonParser;
017import org.hl7.fhir.r5.elementmodel.ObjectConverter;
018import org.hl7.fhir.r5.formats.IParser.OutputStyle;
019import org.hl7.fhir.r5.model.CodeableConcept;
020import org.hl7.fhir.r5.model.Coding;
021import org.hl7.fhir.r5.model.Library;
022import org.hl7.fhir.r5.model.Measure;
023import org.hl7.fhir.r5.model.Measure.MeasureGroupComponent;
024import org.hl7.fhir.r5.model.Measure.MeasureGroupPopulationComponent;
025import org.hl7.fhir.r5.model.Measure.MeasureGroupStratifierComponent;
026import org.hl7.fhir.r5.model.Resource;
027import org.hl7.fhir.r5.renderers.DataRenderer;
028import org.hl7.fhir.r5.utils.ToolingExtensions;
029import org.hl7.fhir.r5.utils.XVerExtensionManager;
030import org.hl7.fhir.utilities.FhirPublication;
031import org.hl7.fhir.utilities.Utilities;
032import org.hl7.fhir.utilities.i18n.I18nConstants;
033import org.hl7.fhir.utilities.validation.ValidationMessage;
034import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
035import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
036import org.hl7.fhir.utilities.xml.XMLUtil;
037import org.hl7.fhir.validation.BaseValidator;
038import org.hl7.fhir.validation.TimeTracker;
039import org.hl7.fhir.validation.instance.InstanceValidator;
040import org.hl7.fhir.validation.instance.utils.NodeStack;
041import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
042import org.w3c.dom.Document;
043
044public class MeasureValidator extends BaseValidator {
045
046  private InstanceValidator parent;
047  public MeasureValidator(IWorkerContext context, TimeTracker timeTracker, XVerExtensionManager xverManager, Coding jurisdiction, InstanceValidator parent) {
048    super(context, xverManager);
049    source = Source.InstanceValidator;
050    this.timeTracker = timeTracker;
051    this.jurisdiction = jurisdiction;
052    this.parent = parent;
053
054  }
055
056  public boolean validateMeasure(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
057    boolean ok = true;
058    MeasureContext mctxt = new MeasureContext();
059    List<Element> libs = element.getChildrenByName("library");
060    for (Element lib : libs) {
061      String ref = lib.isPrimitive() ? lib.primitiveValue() : lib.getChildValue("reference");
062      if (!Utilities.noString(ref)) {
063        Library l = context.fetchResource(Library.class, ref);
064        if (hint(errors, NO_RULE_DATE, IssueType.NOTFOUND, lib.line(), lib.col(), stack.getLiteralPath(), l != null, I18nConstants.MEASURE_M_LIB_UNKNOWN, ref)) {
065          mctxt.seeLibrary(l);
066        }
067      }
068    }
069
070    List<Element> groups = element.getChildrenByName("group");
071    if (warning(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), groups.size() > 0, I18nConstants.MEASURE_M_NO_GROUPS)) {      
072      int c = 0;
073      for (Element group : groups) {
074        NodeStack ns = stack.push(group, c, null, null);
075        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), groups.size() ==1 || group.hasChild("code"), I18nConstants.MEASURE_M_GROUP_CODE);
076        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, group.line(), group.col(), ns.getLiteralPath(), group.hasChildren("population"), I18nConstants.MEASURE_M_GROUP_POP);
077        int c1 = 0;
078        List<Element> pl = group.getChildrenByName("population");
079        for (Element p : pl) {
080          NodeStack ns2 = ns.push(p, c1, null, null);
081          warning(errors, NO_RULE_DATE, IssueType.REQUIRED, p.line(), p.col(), ns2.getLiteralPath(), pl.size() == 1 || p.hasChild("code"), I18nConstants.MEASURE_M_GROUP_POP_NO_CODE);
082          c1++;
083        }
084        c1 = 0;
085        List<Element> stl = group.getChildrenByName("stratifier");
086        for (Element st : stl) {
087          NodeStack ns2 = ns.push(st, c1, null, null);
088          warning(errors, NO_RULE_DATE, IssueType.REQUIRED, st.line(), st.col(), ns2.getLiteralPath(), stl.size() == 1 || st.hasChild("code"), I18nConstants.MEASURE_M_GROUP_STRATA_NO_CODE);
089          if (st.hasChild("criteria")) {
090            Element crit = st.getNamedChild("criteria");
091            NodeStack nsc = ns2.push(crit, -1, null, null);
092            ok = validateMeasureCriteria(hostContext, errors, mctxt, crit, nsc) && ok;
093          }
094          int c2 = 0;
095          List<Element> cpl = group.getChildrenByName("component");
096          for (Element cp : cpl) {
097            NodeStack ns3 = ns2.push(cp, c2, null, null);
098            warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cp.line(), cp.col(), ns3.getLiteralPath(), cpl.size() == 1 || cp.hasChild("code"), I18nConstants.MEASURE_M_GROUP_STRATA_COMP_NO_CODE);
099            if (cp.hasChild("criteria")) {
100              Element crit = cp.getNamedChild("criteria");
101              NodeStack nsc = ns3.push(crit, -1, null, null);
102              ok= validateMeasureCriteria(hostContext, errors, mctxt, crit, nsc) && ok;
103            }
104            c2++;
105          }
106          c1++;
107        }
108        c++;
109      }            
110    }
111    if (!stack.isContained()) {
112      ok = checkShareableMeasure(errors, element, stack) && ok;
113    }
114    return ok;
115  }
116  
117
118  private boolean checkShareableMeasure(List<ValidationMessage> errors, Element cs, NodeStack stack) {
119    boolean ok = true;
120    if (parent.isForPublication()) { 
121      if (isHL7(cs)) {
122        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("url"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "url") && ok;                      
123        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("version"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "version") && ok;                      
124        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("name"), I18nConstants.MEASURE_SHAREABLE_EXTRA_MISSING_HL7, "name") && ok;                      
125        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("title"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "title");                      
126        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("status"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "status") && ok;                      
127        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("experimental"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "experimental") && ok;                      
128        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("publisher"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "publisher") && ok; 
129        ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("description"), I18nConstants.MEASURE_SHAREABLE_MISSING_HL7, "description") && ok; 
130      } else {
131        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("url"), I18nConstants.MEASURE_SHAREABLE_MISSING, "url");                      
132        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("version"), I18nConstants.MEASURE_SHAREABLE_MISSING, "version");                      
133        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("name"), I18nConstants.MEASURE_SHAREABLE_EXTRA_MISSING, "name");                      
134        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("title"), I18nConstants.MEASURE_SHAREABLE_MISSING, "title");                      
135        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("status"), I18nConstants.MEASURE_SHAREABLE_MISSING, "status");                      
136        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("experimental"), I18nConstants.MEASURE_SHAREABLE_MISSING, "experimental");                      
137        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("description"), I18nConstants.MEASURE_SHAREABLE_MISSING, "description"); 
138        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, cs.line(), cs.col(), stack.getLiteralPath(), cs.hasChild("publisher"), I18nConstants.MEASURE_SHAREABLE_MISSING, "publisher"); 
139      }
140    }
141    return ok;
142  }
143  
144  private boolean validateMeasureCriteria(ValidatorHostContext hostContext, List<ValidationMessage> errors, MeasureContext mctxt, Element crit, NodeStack nsc) {
145    boolean ok = true;
146    String mimeType = crit.getChildValue("language");
147    if (!Utilities.noString(mimeType)) { // that would be an error elsewhere 
148      if ("text/cql".equals(mimeType) || "text/cql.identifier".equals(mimeType)) {
149        String cqlRef = crit.getChildValue("expression");
150        Library lib = null;
151        if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), mctxt.libraries().size()> 0, I18nConstants.MEASURE_M_CRITERIA_CQL_NO_LIB)) {
152          if (cqlRef.contains(".")) {
153            String name = cqlRef.substring(0, cqlRef.indexOf(".")); 
154            cqlRef = cqlRef.substring(cqlRef.indexOf(".")+1); 
155            for (Library l : mctxt.libraries()) {
156              if (name.equals(l.getName())) {
157                if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib == null, I18nConstants.MEASURE_M_CRITERIA_CQL_LIB_DUPL)) {
158                  lib = l;
159                } else {
160                  ok = false;
161                }
162              }
163            }
164            ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib != null, I18nConstants.MEASURE_M_CRITERIA_CQL_LIB_NOT_FOUND, name) && ok;
165          } else {
166            if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), mctxt.libraries().size() == 1, I18nConstants.MEASURE_M_CRITERIA_CQL_ONLY_ONE_LIB)) {
167              lib = mctxt.libraries().get(0);
168            } else {
169              ok = false;
170            }
171          }
172        } else {
173          ok = false;
174        }
175        if (lib != null) {
176          if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), lib.hasUserData(MeasureContext.USER_DATA_ELM), I18nConstants.MEASURE_M_CRITERIA_CQL_NO_ELM, lib.getUrl())) {
177            if (lib.getUserData(MeasureContext.USER_DATA_ELM) instanceof String) {
178              ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_CQL_ERROR, lib.getUrl(), lib.getUserString(MeasureContext.USER_DATA_ELM)) && ok;            
179            } else if (lib.getUserData(MeasureContext.USER_DATA_ELM) instanceof Document) {
180              org.w3c.dom.Element elm = ((Document)lib.getUserData(MeasureContext.USER_DATA_ELM)).getDocumentElement();
181              if (rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), isValidElm(elm), I18nConstants.MEASURE_M_CRITERIA_CQL_ELM_NOT_VALID, lib.getUrl(), cqlRef)) {
182                ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, crit.line(), crit.col(), nsc.getLiteralPath(), hasCqlTarget(elm, cqlRef), I18nConstants.MEASURE_M_CRITERIA_CQL_NOT_FOUND, lib.getUrl(), cqlRef) && ok;
183              }
184            }
185          } else {
186            ok = false;
187          }
188        }
189      } else if ("text/fhirpath".equals(mimeType)) {
190        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType);
191      } else if ("application/x-fhir-query".equals(mimeType)) {
192        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType);
193      } else {
194        warning(errors, NO_RULE_DATE, IssueType.REQUIRED, crit.line(), crit.col(), nsc.getLiteralPath(), false, I18nConstants.MEASURE_M_CRITERIA_UNKNOWN, mimeType);
195      }  
196    }
197    return ok;
198  }
199    
200  private boolean isValidElm(org.w3c.dom.Element elm) {
201    return elm != null && "library".equals(elm.getNodeName()) && "urn:hl7-org:elm:r1".equals(elm.getNamespaceURI());
202  }
203
204  private boolean hasCqlTarget(org.w3c.dom.Element element, String cqlRef) {
205    org.w3c.dom.Element stmts = XMLUtil.getNamedChild(element, "statements");
206    if (stmts != null) {
207      for (org.w3c.dom.Element def : XMLUtil.getNamedChildren(stmts, "def")) {
208        if (cqlRef.equals(def.getAttribute("name"))) {
209          return true;
210        }
211      }
212    }
213    return false;
214  }
215
216
217  // ---------------------------------------------------------------------------------------------------------------------------------------------------------
218
219  public boolean validateMeasureReport(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
220    boolean ok = true;
221    Element m = element.getNamedChild("measure");
222    String measure = null;
223    if (m != null) {
224      /*
225       * q.getValue() is correct for R4 content, but we'll also accept the second
226       * option just in case we're validating raw STU3 content. Being lenient here
227       * isn't the end of the world since if someone is actually doing the reference
228       * wrong in R4 content it'll get flagged elsewhere by the validator too
229       */
230      if (isNotBlank(m.getValue())) {
231        measure = m.getValue();
232      } else if (isNotBlank(m.getChildValue("reference"))) {
233        measure = m.getChildValue("reference");
234      }
235    }
236    if (hint(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), measure != null, I18nConstants.MEASURE_MR_M_NONE)) {
237      long t = System.nanoTime();
238      Measure msrc = measure.startsWith("#") ? loadMeasure(element, measure.substring(1)) : context.fetchResource(Measure.class, measure);
239      timeTracker.sd(t);
240      if (warning(errors, NO_RULE_DATE, IssueType.REQUIRED, m.line(), m.col(), stack.getLiteralPath(), msrc != null, I18nConstants.MEASURE_MR_M_NOTFOUND, measure)) {
241        boolean inComplete = !"complete".equals(element.getNamedChildValue("status"));
242        MeasureContext mc = new MeasureContext(msrc, element);
243        NodeStack ns = stack.push(m, -1, m.getProperty().getDefinition(), m.getProperty().getDefinition());
244        hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, m.line(), m.col(), ns.getLiteralPath(), Utilities.existsInList(mc.scoring(), "proportion", "ratio", "continuous-variable", "cohort"), I18nConstants.MEASURE_MR_M_SCORING_UNK); 
245        ok = validateMeasureReportGroups(hostContext, mc, errors, element, stack, inComplete) && ok;
246      }
247    }
248    return ok;
249  }
250
251  private Measure loadMeasure(Element resource, String id) throws FHIRException {
252    try {
253      for (Element contained : resource.getChildren("contained")) {
254        if (contained.getIdBase().equals(id)) {
255          FhirPublication v = FhirPublication.fromCode(context.getVersion());
256          ByteArrayOutputStream bs = new ByteArrayOutputStream();
257          new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id);
258          byte[] json = bs.toByteArray();
259          switch (v) {
260            case DSTU1:
261              throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R1));
262            case DSTU2:
263              throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2));
264            case DSTU2016May:
265              throw new FHIRException(context.formatMessage(I18nConstants.UNSUPPORTED_VERSION_R2B));
266            case STU3:
267              org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json);
268              Resource r5 = VersionConvertorFactory_30_50.convertResource(r3);
269              if (r5 instanceof Measure)
270                return (Measure) r5;
271              else
272                return null;
273            case R4:
274              org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json);
275              r5 = VersionConvertorFactory_40_50.convertResource(r4);
276              if (r5 instanceof Measure)
277                return (Measure) r5;
278              else
279                return null;
280            case R5:
281              r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json);
282              if (r5 instanceof Measure)
283                return (Measure) r5;
284              else
285                return null;
286          }
287        }
288      }
289      return null;
290    } catch (IOException e) {
291      throw new FHIRException(e);
292    }
293  }
294
295  private boolean validateMeasureReportGroups(ValidatorHostContext hostContext, MeasureContext m, List<ValidationMessage> errors, Element mr, NodeStack stack, boolean inProgress) {
296    boolean ok = true;
297    
298    if (m.groups().size() == 0) {
299      // only validate the report groups if the measure has groups.
300      return ok;
301    }
302
303    List<MeasureGroupComponent> groups = new ArrayList<MeasureGroupComponent>();
304
305    List<Element> glist = mr.getChildrenByName("group");
306    
307    if (glist.size() == 1 && m.groups().size() == 1) {
308      // if there's only one group, it can be ((and usually is) anonymous)
309      // but we still check that the code, if both have one, is consistent.
310      Element mrg = glist.get(0);
311      NodeStack ns = stack.push(mrg, 0, mrg.getProperty().getDefinition(), mrg.getProperty().getDefinition());
312      if (m.groups().get(0).hasCode() && mrg.hasChild("code")) {
313        CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrg.getNamedChild("code"));
314        if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), hasUseableCode(cc), I18nConstants.MEASURE_MR_GRP_NO_USABLE_CODE)) {
315          ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), cc.matches(m.groups().get(0).getCode()), I18nConstants.MEASURE_MR_GRP_NO_WRONG_CODE, DataRenderer.display(context, cc), DataRenderer.display(context, m.groups().get(0).getCode())) && ok;
316        } else {
317          ok = false;
318        }
319      }
320      ok = validateMeasureReportGroup(hostContext, m, m.groups().get(0), errors, mrg, ns, inProgress) && ok;
321    } else {
322      int i = 0;
323      for (Element mrg : glist) {
324        NodeStack ns = stack.push(mrg, i, mrg.getProperty().getDefinition(), mrg.getProperty().getDefinition());
325        CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrg.getNamedChild("code"));
326        if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_NO_CODE)) {
327          MeasureGroupComponent mg = getGroupForCode(cc, m.measure());
328          if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mg != null, I18nConstants.MEASURE_MR_GRP_UNK_CODE)) {
329            if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !groups.contains(mg), I18nConstants.MEASURE_MR_GRP_DUPL_CODE)) {
330              groups.add(mg);
331              ok = validateMeasureReportGroup(hostContext, m, mg, errors, mrg, ns, inProgress) && ok;
332            } else {
333              ok = false;
334            }
335          } else {
336            ok = false;
337          }
338        } else {
339          ok = false;
340        }
341        i++;
342      }
343      boolean dataCollection = isDataCollection(mr);
344      for (MeasureGroupComponent mg : m.groups()) {
345        if (!groups.contains(mg)) {
346          ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mr.line(), mr.col(), stack.getLiteralPath(), groups.contains(mg) || dataCollection, I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, DataRenderer.display(context, mg.getCode())) && ok;
347        }
348      }
349    }
350    return ok;
351  }
352
353  private boolean isDataCollection(Element mr) {
354    return "data-collection".equals(mr.getChildValue("type"));
355  }
356
357  private boolean validateMeasureReportGroup(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack ns, boolean inProgress) {
358    boolean ok = true;
359    ok = validateMeasureReportGroupPopulations(hostContext, m, mg, errors, mrg, ns, inProgress) && ok;
360    ok = validateScore(hostContext, m, errors, mrg, ns, inProgress) && ok;
361    ok = validateMeasureReportGroupStratifiers(hostContext, m, mg, errors, mrg, ns, inProgress) && ok;
362    return ok;
363  }
364
365  private boolean validateScore(ValidatorHostContext hostContext, MeasureContext m, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) {
366    boolean ok = true;
367    
368    Element ms = mrg.getNamedChild("measureScore");
369    // first, we check MeasureReport.type
370    if ("data-collection".equals(m.reportType())) {
371      ok = banned(errors, stack, ms, I18nConstants.MEASURE_MR_SCORE_PROHIBITED_RT) && ok;
372    } else if ("cohort".equals(m.scoring())) {
373      //  cohort - there is no measure score
374      ok = banned(errors, stack, ms, I18nConstants.MEASURE_MR_SCORE_PROHIBITED_MS) && ok;
375    } else if (Utilities.existsInList(m.scoring(), "proportion", "ratio", "continuous-variable")) {
376      if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, mrg.line(), mrg.col(), stack.getLiteralPath(), ms != null, I18nConstants.MEASURE_MR_SCORE_REQUIRED, m.scoring())) {
377        NodeStack ns = stack.push(ms, -1, ms.getProperty().getDefinition(), ms.getProperty().getDefinition());
378        Element v = ms.getNamedChild("value");
379        // TODO: this is a DEQM special and should be handled differently
380        if (v == null) {
381          if (ms.hasExtension("http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-alternateScoreType")) {
382            v = ms.getExtension("http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-alternateScoreType").getNamedChild("value");
383          }
384        }
385        if ("proportion".equals(m.scoring())) {
386          //  proportion - score is a unitless number from 0 ... 1
387          ok = banned(errors, ns, ms, "unit", I18nConstants.MEASURE_MR_SCORE_UNIT_PROHIBITED, "proportion");
388          ok = banned(errors, ns, ms, "system", I18nConstants.MEASURE_MR_SCORE_UNIT_PROHIBITED, "proportion");
389          ok = banned(errors, ns, ms, "code", I18nConstants.MEASURE_MR_SCORE_UNIT_PROHIBITED, "proportion");
390          if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, ms.line(), ms.col(), ns.getLiteralPath(), v != null, I18nConstants.MEASURE_MR_SCORE_VALUE_REQUIRED, "proportion")) {
391            try {
392              BigDecimal dec = new BigDecimal(v.primitiveValue());
393              NodeStack nsv = ns.push(v, -1, v.getProperty().getDefinition(), v.getProperty().getDefinition());
394              ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, v.line(), v.col(), nsv.getLiteralPath(), dec.compareTo(new BigDecimal(0)) >= 0 && dec.compareTo(new BigDecimal(1)) <= 0, I18nConstants.MEASURE_MR_SCORE_VALUE_INVALID_01) && ok;
395            } catch (Exception e) {
396              // nothing - will have caused an error elsewhere
397            }            
398          } else {
399            ok = false;
400          }
401        } else if ("ratio".equals(m.scoring())) {
402          //  ratio -  score is a number with no value constraints, and maybe with a unit (perhaps constrained by extension)
403          if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, ms.line(), ms.col(), ns.getLiteralPath(), v != null, I18nConstants.MEASURE_MR_SCORE_VALUE_REQUIRED, "ratio")) {
404            Element unit = ms.getNamedChild("code");
405            Coding c = m.measure().hasExtension(ToolingExtensions.EXT_Q_UNIT) ? (Coding) m.measure().getExtensionByUrl(ToolingExtensions.EXT_Q_UNIT).getValue() : null;
406            if (unit != null) {
407              if (c != null) {
408                NodeStack nsc = ns.push(unit, -1, unit.getProperty().getDefinition(), unit.getProperty().getDefinition());
409                ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, unit.line(), unit.col(), nsc.getLiteralPath(), c.getCode().equals(unit.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getCode()) && ok;
410                Element system = ms.getNamedChild("system");
411                if (system == null) {
412                  NodeStack nss = system == null ? ns : ns.push(system, -1, system.getProperty().getDefinition(), system.getProperty().getDefinition());
413                  ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, system.line(), system.col(), nss.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok;
414                } else {
415                  ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, ms.line(), ms.col(), ns.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok;
416                }
417              }
418            } else if (c != null) {
419              ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, ms.line(), ms.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_SCORE_FIXED, DataRenderer.display(context, c)) && ok;            
420            } else {
421              warning(errors, NO_RULE_DATE, IssueType.NOTFOUND, ms.line(), ms.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_SCORE_UNIT_REQUIRED, "ratio");            
422            }
423          } else {
424            ok = true;
425          }
426        } else if ("continuous-variable".equals(m.scoring())) {
427          // continuous-variable - score is a quantity with a unit per the extension
428          if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, ms.line(), ms.col(), ns.getLiteralPath(), v != null, I18nConstants.MEASURE_MR_SCORE_VALUE_REQUIRED, "continuous-variable")) {
429            Element unit = ms.getNamedChild("code");
430            Coding c = m.measure().hasExtension(ToolingExtensions.EXT_Q_UNIT) ? (Coding) m.measure().getExtensionByUrl(ToolingExtensions.EXT_Q_UNIT).getValue() : null;
431            if (unit != null) {
432              if (c != null) {
433                NodeStack nsc = ns.push(unit, -1, unit.getProperty().getDefinition(), unit.getProperty().getDefinition());
434                rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, unit.line(), unit.col(), nsc.getLiteralPath(), c.getCode().equals(unit.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getCode());
435                Element system = ms.getNamedChild("system");
436                if (system == null) {
437                  NodeStack nss = system == null ? ns : ns.push(system, -1, system.getProperty().getDefinition(), system.getProperty().getDefinition());
438                  ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, system.line(), system.col(), nss.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok;
439                } else {
440                  ok = rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, ms.line(), ms.col(), ns.getLiteralPath(), c.getSystem().equals(system.primitiveValue()), I18nConstants.MEASURE_MR_SCORE_FIXED, c.getSystem()) && ok;
441                }
442              }
443            } else if (c != null) {
444              ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, ms.line(), ms.col(), ns.getLiteralPath(), false, I18nConstants.MEASURE_MR_SCORE_FIXED, DataRenderer.display(context, c)) && ok;            
445            } 
446          }
447        }
448      } else { // else do nothing - there's a hint elsewhere
449        ok = false;
450      }
451    } 
452    return ok;
453  }
454
455  private boolean banned(List<ValidationMessage> errors, NodeStack stack, Element parent, String childName, String msgId, Object... params) {
456    Element child = parent.getNamedChild(childName);
457    return banned(errors, stack, child, msgId, params);
458  }
459  
460  private boolean banned(List<ValidationMessage> errors, NodeStack stack, Element e, String msgId, Object... params) {
461    if (e != null) {
462      NodeStack ns = stack.push(e, -1, e.getProperty().getDefinition(), e.getProperty().getDefinition());
463      rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, e.line(), e.col(), ns.getLiteralPath(), false, msgId, params);
464      return false;
465    } else {
466      return true;
467    }
468  }
469  private boolean validateMeasureReportGroupPopulations(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) {
470    boolean ok = true;
471    // there must be a population for each population defined in the measure, and no 4others. 
472    List<MeasureGroupPopulationComponent> pops = new ArrayList<MeasureGroupPopulationComponent>();
473    List<Element> plist = mrg.getChildrenByName("population");
474    
475    int i = 0;
476    for (Element mrgp : plist) {
477      NodeStack ns = stack.push(mrgp, i, mrgp.getProperty().getDefinition(), mrgp.getProperty().getDefinition());
478      CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgp.getNamedChild("code"));
479      if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) {
480        MeasureGroupPopulationComponent mgp = getGroupPopForCode(cc, mg);
481        if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgp != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) {
482          if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !pops.contains(mgp), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) {
483            pops.add(mgp);
484            ok = validateMeasureReportGroupPopulation(hostContext, m, mgp, errors, mrgp, ns, inProgress) && ok;
485          } else {
486            ok = false;
487          }
488        } else {
489          ok = false;
490        }
491      } else {
492        ok = false;
493      }
494      i++;
495    }
496    for (MeasureGroupPopulationComponent mgp : mg.getPopulation()) {
497      if (!pops.contains(mgp) && !mgp.getCode().hasCoding("http://terminology.hl7.org/CodeSystem/measure-population", "measure-observation")) {
498        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), stack.getLiteralPath(), pops.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, DataRenderer.display(context, mgp.getCode())) && ok;
499      }
500    }
501    return ok;
502  }
503  
504  private boolean validateMeasureReportGroupPopulation(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupPopulationComponent mgp, List<ValidationMessage> errors, Element mrgp, NodeStack ns, boolean inProgress) {
505    boolean ok = true;
506    List<Element> sr = mrgp.getChildrenByName("subjectResults");
507    if ("subject-list".equals(m.reportType())) {
508      try {
509        int c = Integer.parseInt(mrgp.getChildValue("count"));
510        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), c == sr.size(), I18nConstants.MEASURE_MR_GRP_POP_COUNT_MISMATCH, c, sr.size()) && ok;
511      } catch (Exception e) {
512        // nothing; that'll be because count is not valid, and that's a different error or its missing and we don't care
513      }
514    } else {
515      ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), sr.size() == 0, I18nConstants.MEASURE_MR_GRP_POP_NO_SUBJECTS) && ok;
516      warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgp.line(), mrgp.col(), ns.getLiteralPath(), mrgp.hasChild("count"), I18nConstants.MEASURE_MR_GRP_POP_NO_COUNT);      
517    }
518    return ok;
519  }
520
521  private boolean validateMeasureReportGroupStratifiers(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupComponent mg, List<ValidationMessage> errors, Element mrg, NodeStack stack, boolean inProgress) {
522    boolean ok = true;
523    
524    // there must be a population for each population defined in the measure, and no 4others. 
525    List<MeasureGroupStratifierComponent> strats = new ArrayList<>();
526    List<Element> slist = mrg.getChildrenByName("stratifier");
527    
528    int i = 0;
529    for (Element mrgs : slist) {
530      NodeStack ns = stack.push(mrgs, i, mrgs.getProperty().getDefinition(), mrgs.getProperty().getDefinition());
531      CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgs.getNamedChild("code"));
532      if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgs.line(), mrgs.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) {
533        MeasureGroupStratifierComponent mgs = getGroupStratifierForCode(cc, mg);
534        if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgs != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) {
535          if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !strats.contains(mgs), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) {
536            strats.add(mgs);
537            ok = validateMeasureReportGroupStratifier(hostContext, m, mgs, errors, mrgs, ns, inProgress) && ok;
538          } else {
539            ok = false;
540          }
541        } else {
542          ok = false;
543        }
544      } else {
545        ok = false;
546      }
547      i++;
548    }
549    for (MeasureGroupStratifierComponent mgs : mg.getStratifier()) {
550      if (!strats.contains(mgs)) {
551        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), stack.getLiteralPath(), strats.contains(mg), I18nConstants.MEASURE_MR_GRP_MISSING_BY_CODE, DataRenderer.display(context, mgs.getCode())) && ok;
552      }
553    }
554    return true;
555  }
556  
557  private boolean validateMeasureReportGroupStratifier(ValidatorHostContext hostContext, MeasureContext m, MeasureGroupStratifierComponent mgs, List<ValidationMessage> errors, Element mrgs, NodeStack ns, boolean inProgress) {
558    // still to be done
559    return true;
560  }
561
562  private MeasureGroupStratifierComponent getGroupStratifierForCode(CodeableConcept cc, MeasureGroupComponent mg) {
563    for (MeasureGroupStratifierComponent t : mg.getStratifier()) {
564      if (t.hasCode()) {
565        for (Coding c : t.getCode().getCoding()) {
566          if (cc.hasCoding(c.getSystem(), c.getCode())) {
567            return t;
568          }
569        }
570        if (!cc.hasCoding() && !t.getCode().hasCoding()) {
571          if (cc.hasText() && t.getCode().hasText()) {
572            if (cc.getText().equals(t.getCode().getText())) {
573              return t;
574            }
575          }
576        }
577      }
578    }
579    return null;
580  }
581
582  private boolean hasUseableCode(CodeableConcept cc) {
583    for (Coding c : cc.getCoding()) {
584      if (c.hasSystem() && c.hasCode()) {
585        return true;
586      }
587    }
588    return false;
589  }
590
591  private MeasureGroupPopulationComponent getGroupPopForCode(CodeableConcept cc, MeasureGroupComponent mg) {
592    for (MeasureGroupPopulationComponent t : mg.getPopulation()) {
593      if (t.hasCode()) {
594        for (Coding c : t.getCode().getCoding()) {
595          if (cc.hasCoding(c.getSystem(), c.getCode())) {
596            return t;
597          }
598        }
599      }
600    }
601    return null;
602  }
603  private MeasureGroupComponent getGroupForCode(CodeableConcept cc, Measure m) {
604    for (MeasureGroupComponent t : m.getGroup()) {
605      if (t.hasCode()) {
606        for (Coding c : t.getCode().getCoding()) {
607          if (cc.hasCoding(c.getSystem(), c.getCode())) {
608            return t;
609          }
610        }
611      }
612    }
613    return null;
614  }
615
616
617}