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