001package org.hl7.fhir.r5.comparison;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Date;
006import java.util.List;
007
008import org.hl7.fhir.exceptions.DefinitionException;
009import org.hl7.fhir.exceptions.FHIRException;
010import org.hl7.fhir.r5.context.IWorkerContext;
011import org.hl7.fhir.r5.model.CanonicalType;
012import org.hl7.fhir.r5.model.Element;
013import org.hl7.fhir.r5.model.ValueSet;
014import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
015import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
016import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
017import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent;
018import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
019import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
020import org.hl7.fhir.utilities.Utilities;
021import org.hl7.fhir.utilities.i18n.RenderingI18nContext;
022import org.hl7.fhir.utilities.validation.ValidationMessage;
023import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
024import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
025import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
026import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
027import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
028import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
029import org.hl7.fhir.utilities.xhtml.NodeType;
030import org.hl7.fhir.utilities.xhtml.XhtmlNode;
031
032public class ValueSetComparer extends CanonicalResourceComparer {
033
034  public class ValueSetComparison extends CanonicalResourceComparison<ValueSet> {
035
036    public ValueSetComparison(ValueSet left, ValueSet right) {
037      super(left, right);
038    }
039    
040    private StructuralMatch<Element> includes = new StructuralMatch<>();       
041    private StructuralMatch<Element> excludes = new StructuralMatch<>();       
042    private StructuralMatch<ValueSetExpansionContainsComponent> expansion;
043    
044    public StructuralMatch<Element> getIncludes() {
045      return includes;
046    }
047    
048    public StructuralMatch<Element> getExcludes() {
049      return excludes;
050    }
051    
052    public StructuralMatch<ValueSetExpansionContainsComponent> getExpansion() {
053      return expansion;
054    }         
055
056    public StructuralMatch<ValueSetExpansionContainsComponent> forceExpansion() {
057      if (expansion == null) {
058        expansion = new StructuralMatch<>();
059      }
060      return expansion;
061    }
062
063    @Override
064    protected String abbreviation() {
065      return "vs";
066    }
067
068    @Override
069    protected String summary() {
070      String res = "ValueSet: "+left.present()+" vs "+right.present();
071      String ch = changeSummary();
072      if (ch != null) {
073        res = res + ". "+ch;
074      }
075      return res;
076    }
077
078    @Override
079    protected String fhirType() {
080      return "ValueSet";
081    }         
082    @Override
083    protected void countMessages(MessageCounts cnts) {
084      super.countMessages(cnts);
085      if (includes != null) {
086        includes.countMessages(cnts);
087      }
088      if (excludes != null) {
089        excludes.countMessages(cnts);
090      }
091      if (expansion != null) {
092        expansion.countMessages(cnts);
093      }
094    }
095
096  }
097  
098  public ValueSetComparer(ComparisonSession session) {
099    super(session);
100  }
101
102  public ValueSetComparison compare(ValueSet left, ValueSet right) {  
103    if (left == null)
104      throw new DefinitionException("No ValueSet provided (left)");
105    if (right == null)
106      throw new DefinitionException("No ValueSet provided (right)");
107    
108    ValueSetComparison res = new ValueSetComparison(left, right);
109    session.identify(res);
110    ValueSet vs = new ValueSet();
111    res.setUnion(vs);
112    session.identify(vs);
113    vs.setName("Union"+left.getName()+"And"+right.getName());
114    vs.setTitle("Union of "+left.getTitle()+" And "+right.getTitle());
115    vs.setStatus(left.getStatus());
116    vs.setDate(new Date());
117   
118    ValueSet vs1 = new ValueSet();
119    res.setIntersection(vs1);
120    session.identify(vs1);
121    vs1.setName("Intersection"+left.getName()+"And"+right.getName());
122    vs1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle());
123    vs1.setStatus(left.getStatus());
124    vs1.setDate(new Date());
125   
126    List<String> chMetadata = new ArrayList<>();
127    var ch = compareMetadata(left, right, res.getMetadata(), res, chMetadata, right);
128    var def = false;
129    if (comparePrimitives("immutable", left.getImmutableElement(), right.getImmutableElement(), res.getMetadata(), IssueSeverity.WARNING, res)) {
130      ch = true;
131      chMetadata.add("immutable");
132    }
133    if (left.hasCompose() || right.hasCompose()) {
134      if (comparePrimitives("compose.lockedDate", left.getCompose().getLockedDateElement(), right.getCompose().getLockedDateElement(), res.getMetadata(), IssueSeverity.WARNING, res)) {
135        ch = true;
136        chMetadata.add("compose.lockedDate");
137      }
138      def = comparePrimitives("compose.inactive", left.getCompose().getInactiveElement(), right.getCompose().getInactiveElement(), res.getMetadata(), IssueSeverity.WARNING, res) || def;      
139    }
140    res.updatedMetadataState(ch, chMetadata);
141        
142    def = compareCompose(left.getCompose(), right.getCompose(), res, res.getUnion().getCompose(), res.getIntersection().getCompose()) || def;
143    res.updateDefinitionsState(def);
144//    compareExpansions(left, right, res);
145    session.annotate(right, res);
146    return res;
147  }
148
149  private boolean compareCompose(ValueSetComposeComponent left, ValueSetComposeComponent right, ValueSetComparison res, ValueSetComposeComponent union, ValueSetComposeComponent intersection) {
150    boolean def = false;
151    // first, the includes
152    List<ConceptSetComponent> matchR = new ArrayList<>();
153    for (ConceptSetComponent l : left.getInclude()) {
154      ConceptSetComponent r = findInList(right.getInclude(), l, left.getInclude());
155      if (r == null) {
156        union.getInclude().add(l);
157        res.updateContentState(true);
158        res.getIncludes().getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.INFORMATION, "Removed Include", "ValueSet.compose.include")));
159        session.markDeleted(right,  "include", l);
160      } else {
161        matchR.add(r);
162        ConceptSetComponent csM = new ConceptSetComponent();
163        ConceptSetComponent csI = new ConceptSetComponent();
164        union.getInclude().add(csM);
165        intersection.getInclude().add(csI);
166        StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r);
167        res.getIncludes().getChildren().add(sm);
168        def = compareDefinitions("ValueSet.compose.exclude["+right.getInclude().indexOf(r)+"]", l, r, sm, csM, csI, res) || def;
169      }
170    }
171    for (ConceptSetComponent r : right.getInclude()) {
172      if (!matchR.contains(r)) {
173        union.getInclude().add(r);
174        res.updateContentState(true);
175        res.getIncludes().getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.INFORMATION, "Added Include", "ValueSet.compose.include"), r));  
176        session.markAdded(r);
177      }
178    }
179    
180    // now. the excludes
181    matchR.clear();
182    for (ConceptSetComponent l : left.getExclude()) {
183      ConceptSetComponent r = findInList(right.getExclude(), l, left.getExclude());
184      if (r == null) {
185        union.getExclude().add(l);
186        res.updateContentState(true);
187        res.getExcludes().getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.INFORMATION, "Removed Exclude", "ValueSet.compose.exclude")));
188      } else {
189        matchR.add(r);
190        ConceptSetComponent csM = new ConceptSetComponent();
191        ConceptSetComponent csI = new ConceptSetComponent();
192        union.getExclude().add(csM);
193        intersection.getExclude().add(csI);
194        StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r);
195        res.getExcludes().getChildren().add(sm);
196        def = compareDefinitions("ValueSet.compose.exclude["+right.getExclude().indexOf(r)+"]", l, r, sm, csM, csI, res) || def;
197      }
198    }
199    for (ConceptSetComponent r : right.getExclude()) {
200      if (!matchR.contains(r)) {
201        union.getExclude().add(r);
202        res.updateContentState(true);
203        res.getExcludes().getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.INFORMATION, "Added Exclude", "ValueSet.compose.exclude"), r));        
204      }
205    }
206    return def;
207  }
208
209  private ConceptSetComponent findInList(List<ConceptSetComponent> matches, ConceptSetComponent item, List<ConceptSetComponent> source) {
210    if (matches.size() == 1 && source.size() == 1) {
211      return matches.get(0);      
212    }
213    int matchCount = countMatchesBySystem(matches, item); 
214    int sourceCount = countMatchesBySystem(source, item); 
215
216    if (matchCount == 1 && sourceCount == 1) {
217      for (ConceptSetComponent t : matches) {
218        if (t.getSystem() != null && t.getSystem().equals(item.getSystem())) {
219          return t;
220        }
221      }
222    }
223    // if there's more than one candidate match by system, then we look for a full match
224    for (ConceptSetComponent t : matches) {
225      if (t.equalsDeep(item)) {
226        return t;
227      }
228    }
229    return null;
230  }
231
232  private int countMatchesBySystem(List<ConceptSetComponent> list, ConceptSetComponent item) {
233    int c = 0;
234    for (ConceptSetComponent t : list) {
235      if (t.hasSystem() && t.getSystem().equals(item.getSystem())) {
236        c++;
237      }
238    }
239    return c;
240  }
241
242
243  private boolean compareDefinitions(String path, ConceptSetComponent left, ConceptSetComponent right, StructuralMatch<Element> combined, ConceptSetComponent union, ConceptSetComponent intersection, ValueSetComparison res) {
244    boolean def = false;
245    // system must match, but the rest might not. we're going to do the full comparison whatever, so the outcome looks consistent to the user    
246    List<CanonicalType> matchVSR = new ArrayList<>();
247    for (CanonicalType l : left.getValueSet()) {
248      CanonicalType r = findInList(right.getValueSet(), l, left.getValueSet());
249      if (r == null) {
250        union.getValueSet().add(l);
251        res.updateContentState(true);
252        combined.getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.ERROR, "Removed ValueSet", "ValueSet.compose.include.valueSet")));
253        if (session.isAnnotate()) {
254          session.markDeleted(right,  "valueset", l);
255        }
256      } else {
257        matchVSR.add(r);
258        if (l.getValue().equals(r.getValue())) {
259          union.getValueSet().add(l);
260          intersection.getValueSet().add(l);
261          StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, null);
262          combined.getChildren().add(sm);          
263        } else {
264          // it's not possible to get here?
265          union.getValueSet().add(l);
266          union.getValueSet().add(r);
267          res.updateContentState(true);
268          StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, vmI(IssueSeverity.WARNING, "Values are different", "ValueSet.compose.include.valueSet"));
269          combined.getChildren().add(sm);            
270          if (session.isAnnotate()) {
271            session.markChanged(r,  l);
272          }           
273
274        }
275      }
276    }
277    for (CanonicalType r : right.getValueSet()) {
278      if (!matchVSR.contains(r)) {
279        union.getValueSet().add(r);
280        res.updateContentState(true);
281        combined.getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.ERROR, "Add ValueSet", "ValueSet.compose.include.valueSet"), r));  
282        session.markAdded(r);
283      }
284    }
285    
286    List<ConceptReferenceComponent> matchCR = new ArrayList<>();
287    for (ConceptReferenceComponent l : left.getConcept()) {
288      ConceptReferenceComponent r = findInList(right.getConcept(), l, left.getConcept());
289      if (r == null) {
290        union.getConcept().add(l);
291        res.updateContentState(true);
292        combined.getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.ERROR, "Removed this Concept", "ValueSet.compose.include.concept")));
293        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" removed", IssueSeverity.ERROR));
294        session.markDeleted(right,"concept", l);
295      } else {
296        matchCR.add(r);
297        if (l.getCode().equals(r.getCode())) {
298          ConceptReferenceComponent cu = new ConceptReferenceComponent();
299          ConceptReferenceComponent ci = new ConceptReferenceComponent();
300          union.getConcept().add(cu);
301          intersection.getConcept().add(ci);
302          StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r);
303          combined.getChildren().add(sm);
304          def = compareConcepts(path+".concept["+right.getConcept().indexOf(r)+"]", l, r, sm, cu, ci, res) || def;
305        } else {
306          // not that it's possible to get here?
307          union.getConcept().add(l);
308          union.getConcept().add(r);
309          StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, vmI(IssueSeverity.WARNING, "Concepts are different", "ValueSet.compose.include.concept"));
310          combined.getChildren().add(sm);
311          res.updateContentState(true);
312          compareConcepts(path+".concept["+right.getConcept().indexOf(r)+"]", l, r, sm, null, null, res);
313          session.markChanged(r, l);
314        }
315      }
316    }
317    for (ConceptReferenceComponent r : right.getConcept()) {
318      if (!matchCR.contains(r)) {
319        union.getConcept().add(r);
320        res.updateContentState(true);
321        combined.getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.ERROR, "Added this Concept", "ValueSet.compose.include.concept"), r)); 
322        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+r.getCode()+" added", IssueSeverity.ERROR));
323        session.markAdded(r);
324      }
325    }
326    
327    List<ConceptSetFilterComponent> matchFR = new ArrayList<>();
328    for (ConceptSetFilterComponent l : left.getFilter()) {
329      ConceptSetFilterComponent r = findInList(right.getFilter(), l, left.getFilter());
330      if (r == null) {
331        union.getFilter().add(l);
332        res.updateContentState(true);
333        combined.getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.ERROR, "Removed this item", "ValueSet.compose.include.filter")));
334        session.markDeleted(right, "filter", l);
335      } else {
336        matchFR.add(r);
337        if (l.getProperty().equals(r.getProperty()) && l.getOp().equals(r.getOp())) {
338          ConceptSetFilterComponent cu = new ConceptSetFilterComponent();
339          ConceptSetFilterComponent ci = new ConceptSetFilterComponent();
340          union.getFilter().add(cu);
341          intersection.getFilter().add(ci);
342          StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r);
343          combined.getChildren().add(sm);
344          if (!compareFilters(l, r, sm, cu, ci)) {
345            res.updateContentState(true);       
346            session.markChanged(r, l);
347          }
348        } else {
349          union.getFilter().add(l);
350          union.getFilter().add(r);
351          StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, vmI(IssueSeverity.WARNING, "Codes are different", "ValueSet.compose.include.filter"));
352          res.updateContentState(true);            
353          combined.getChildren().add(sm);
354          compareFilters(l, r, sm, null, null);
355        }
356      }
357    }
358    for (ConceptSetFilterComponent r : right.getFilter()) {
359      if (!matchFR.contains(r)) {
360        union.getFilter().add(r);
361        res.updateContentState(true);
362        combined.getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.ERROR, "Added this item", "ValueSet.compose.include.filter"), r));  
363        session.markAdded(r);
364      }
365    }
366    return def;
367  }
368
369  private boolean compareConcepts(String path, ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch<Element> sm, ConceptReferenceComponent cu,  ConceptReferenceComponent ci, ValueSetComparison res) {
370    boolean def = false;
371    sm.getChildren().add(new StructuralMatch<Element>(l.getCodeElement(), r.getCodeElement(), l.getCode().equals(r.getCode()) ? null : vmI(IssueSeverity.INFORMATION, "Codes do not match", "ValueSet.compose.include.concept")));
372    if (ci != null) {
373      ci.setCode(l.getCode());
374      cu.setCode(l.getCode());
375    }
376    if (l.hasDisplay() && r.hasDisplay()) {
377      sm.getChildren().add(new StructuralMatch<Element>(l.getDisplayElement(), r.getDisplayElement(), l.getDisplay().equals(r.getDisplay()) ? null : vmI(IssueSeverity.INFORMATION, "Displays do not match", "ValueSet.compose.include.concept")));
378      if (ci != null) {
379        ci.setDisplay(r.getDisplay());
380        cu.setDisplay(r.getDisplay());
381      }
382      def = !l.getDisplay().equals(r.getDisplay());
383      if (def) {
384        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display changed from '"+l.getDisplay()+"' to '"+r.getDisplay()+"'", IssueSeverity.WARNING));
385        session.markChanged(r.getDisplayElement(),  l.getDisplayElement());
386      }
387    } else if (l.hasDisplay()) {
388      session.markDeleted(r, "display", l.getDisplayElement());
389      res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display '"+l.getDisplay()+"' removed", IssueSeverity.WARNING));
390      sm.getChildren().add(new StructuralMatch<Element>(l.getDisplayElement(), null, vmI(IssueSeverity.INFORMATION, "Display Removed", "ValueSet.compose.include.concept")));
391      if (ci != null) {
392        ci.setDisplay(l.getDisplay());
393        cu.setDisplay(l.getDisplay());
394      }
395      def = true;
396    } else if (r.hasDisplay()) {
397      session.markAdded(r.getDisplayElement());
398      res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display '"+r.getDisplay()+"' added", IssueSeverity.WARNING));
399      sm.getChildren().add(new StructuralMatch<Element>(null, r.getDisplayElement(), vmI(IssueSeverity.INFORMATION, "Display added", "ValueSet.compose.include.concept")));
400      if (ci != null) {
401        ci.setDisplay(r.getDisplay());
402        cu.setDisplay(r.getDisplay());
403      }
404      def = true;
405    } else {
406      sm.getChildren().add(new StructuralMatch<Element>(null, null, vmI(IssueSeverity.INFORMATION, "No Display", "ValueSet.compose.include.concept")));
407    }
408    return def;
409  }
410
411  private boolean compareFilters(ConceptSetFilterComponent l, ConceptSetFilterComponent r, StructuralMatch<Element> sm, ConceptSetFilterComponent cu,  ConceptSetFilterComponent ci) {
412    sm.getChildren().add(new StructuralMatch<Element>(l.getPropertyElement(), r.getPropertyElement(), l.getProperty().equals(r.getProperty()) ? null : vmI(IssueSeverity.INFORMATION, "Properties do not match", "ValueSet.compose.include.concept")));
413    sm.getChildren().add(new StructuralMatch<Element>(l.getOpElement(), r.getOpElement(), l.getOp().equals(r.getOp()) ? null : vmI(IssueSeverity.INFORMATION, "Filter Operations do not match", "ValueSet.compose.include.concept")));
414    sm.getChildren().add(new StructuralMatch<Element>(l.getValueElement(), r.getValueElement(), l.getValue().equals(r.getValue()) ? null : vmI(IssueSeverity.INFORMATION, "Values do not match", "ValueSet.compose.include.concept")));
415    if (ci != null) {
416      ci.setProperty(l.getProperty());
417      ci.setOp(l.getOp());
418      ci.setValue(l.getValue());
419      cu.setProperty(l.getProperty());
420      cu.setOp(l.getOp());
421      cu.setValue(l.getValue());
422    }
423    return !l.getProperty().equals(r.getProperty());
424  }
425  
426  private CanonicalType findInList(List<CanonicalType> matches, CanonicalType item, List<CanonicalType> source) {
427    if (matches.size() == 1 && source.size() == 1) {
428      return matches.get(0);      
429    }
430    for (CanonicalType t : matches) {
431      if (t.getValue().equals(item.getValue())) {
432        return t;
433      }
434    }
435    return null;
436  }
437
438  private ConceptReferenceComponent findInList(List<ConceptReferenceComponent> matches, ConceptReferenceComponent item, List<ConceptReferenceComponent> source) {
439    if (matches.size() == 1 && source.size() == 1) {
440      return matches.get(0);      
441    }
442    for (ConceptReferenceComponent t : matches) {
443      if (t.getCode().equals(item.getCode())) {
444        return t;
445      }
446    }
447    return null;
448  }
449
450  private ConceptSetFilterComponent findInList(List<ConceptSetFilterComponent> matches, ConceptSetFilterComponent item, List<ConceptSetFilterComponent> source) {
451    if (matches.size() == 1 && source.size() == 1) {
452      return matches.get(0);      
453    }
454    for (ConceptSetFilterComponent t : matches) {
455      if (t.getProperty().equals(item.getProperty()) && t.getOp().equals(item.getOp()) ) {
456        return t;
457      }
458    }
459    return null;
460  }
461
462  private void compareExpansions(ValueSet left, ValueSet right, ValueSetComparison res) {
463    ValueSet expL = left.hasExpansion() ? left : expand(left, res, "left", session.getContextLeft());
464    ValueSet expR = right.hasExpansion() ? right : expand(right, res, "right", session.getContextRight());
465    if (expL != null && expR != null) {
466      // ignore the parameters for now
467      compareConcepts(expL.getExpansion().getContains(), expR.getExpansion().getContains(), res.forceExpansion(), res.getUnion().getExpansion().getContains(), res.getIntersection().getExpansion().getContains(), "ValueSet.expansion.contains", res);
468    }
469  }
470  
471  private ValueSet expand(ValueSet vs, ValueSetComparison res, String name, IWorkerContext ctxt) {
472    ValueSetExpansionOutcome vse = ctxt.expandVS(vs, true, false);
473    if (vse.getValueset() != null) {
474      return vse.getValueset();
475    } else {
476      res.getMessages().add(new ValidationMessage(Source.TerminologyEngine, IssueType.EXCEPTION, "ValueSet", "Error Expanding "+name+":"+vse.getError(), IssueSeverity.ERROR));
477      return null;
478    }
479  }  
480
481  private void compareConcepts(List<ValueSetExpansionContainsComponent> left, List<ValueSetExpansionContainsComponent> right, StructuralMatch<ValueSetExpansionContainsComponent> combined, List<ValueSetExpansionContainsComponent> union, List<ValueSetExpansionContainsComponent> intersection, String path, ValueSetComparison res) {
482    List<ValueSetExpansionContainsComponent> matchR = new ArrayList<>();
483    for (ValueSetExpansionContainsComponent l : left) {
484      ValueSetExpansionContainsComponent r = findInList(right, l);
485      if (r == null) {
486        union.add(l);
487        combined.getChildren().add(new StructuralMatch<ValueSetExpansionContainsComponent>(l, vmI(IssueSeverity.INFORMATION, "Removed from expansion", path)));
488      } else {
489        matchR.add(r);
490        ValueSetExpansionContainsComponent ccU = merge(l, r);
491        ValueSetExpansionContainsComponent ccI = intersect(l, r);
492        union.add(ccU);
493        intersection.add(ccI);
494        StructuralMatch<ValueSetExpansionContainsComponent> sm = new StructuralMatch<ValueSetExpansionContainsComponent>(l, r);
495        compareItem(sm.getMessages(), path, l, r, res);
496        combined.getChildren().add(sm);
497        compareConcepts(l.getContains(), r.getContains(), sm, ccU.getContains(), ccI.getContains(), path+".where(code = '"+l.getCode()+"').contains", res);
498      }
499    }
500    for (ValueSetExpansionContainsComponent r : right) {
501      if (!matchR.contains(r)) {
502        union.add(r);
503        combined.getChildren().add(new StructuralMatch<ValueSetExpansionContainsComponent>(vmI(IssueSeverity.INFORMATION, "Added to expansion", path), r));        
504      }
505    }
506  }
507
508  private void compareItem(List<ValidationMessage> msgs, String path, ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r, ValueSetComparison res) {
509    compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res);
510  }
511
512  private void compareStrings(String path, List<ValidationMessage> msgs, String left, String right, String name, IssueSeverity level, ValueSetComparison res) {
513    if (!Utilities.noString(right)) {
514      if (Utilities.noString(left)) {
515        msgs.add(vmI(level, "Value for "+name+" added", path));
516      } else if (!left.equals(right)) {
517        if (level != IssueSeverity.NULL) {
518          res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path+".name", "Changed value for "+name+": '"+left+"' vs '"+right+"'", level));
519        }
520        msgs.add(vmI(level, name+" changed from left to right", path));
521      }
522    } else if (!Utilities.noString(left)) {
523      msgs.add(vmI(level, "Value for "+name+" removed", path));
524    }
525  }
526
527  private ValueSetExpansionContainsComponent findInList(List<ValueSetExpansionContainsComponent> list, ValueSetExpansionContainsComponent item) {
528    for (ValueSetExpansionContainsComponent t : list) {
529      if (t.getSystem().equals(item.getSystem()) && t.getCode().equals(item.getCode())) {
530        return t;
531      }
532    }
533    return null;
534  }
535
536  private ValueSetExpansionContainsComponent intersect(ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r) {
537    ValueSetExpansionContainsComponent res = new ValueSetExpansionContainsComponent();
538    if (l.hasAbstract() && r.hasAbstract()) {
539      res.setAbstract(l.getAbstract());
540    }
541    if (l.hasCode() && r.hasCode()) {
542      res.setCode(l.getCode());
543    }
544    if (l.hasSystem() && r.hasSystem()) {
545      res.setSystem(l.getSystem());
546    }
547    if (l.hasVersion() && r.hasVersion()) {
548      res.setVersion(l.getVersion());
549    }
550    if (l.hasDisplay() && r.hasDisplay()) {
551      res.setDisplay(l.getDisplay());
552    }
553    return res;
554  }
555
556  private ValueSetExpansionContainsComponent merge(ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r) {
557    ValueSetExpansionContainsComponent res = new ValueSetExpansionContainsComponent();
558    if (l.hasAbstract()) {
559      res.setAbstract(l.getAbstract());
560    } else if (r.hasAbstract()) {
561      res.setAbstract(r.getAbstract());
562    }
563    if (l.hasCode()) {
564      res.setCode(l.getCode());
565    } else if (r.hasCode()) {
566      res.setCode(r.getCode());
567    }
568    if (l.hasSystem()) {
569      res.setSystem(l.getSystem());
570    } else if (r.hasSystem()) {
571      res.setSystem(r.getSystem());
572    }
573    if (l.hasVersion()) {
574      res.setVersion(l.getVersion());
575    } else if (r.hasVersion()) {
576      res.setVersion(r.getVersion());
577    }
578    if (l.hasDisplay()) {
579      res.setDisplay(l.getDisplay());
580    } else if (r.hasDisplay()) {
581      res.setDisplay(r.getDisplay());
582    }
583    return res;
584  }
585
586  @Override
587  protected String fhirType() {
588    return "ValueSet";
589  }
590
591  public XhtmlNode renderCompose(ValueSetComparison csc, String id, String prefix) throws FHIRException, IOException {
592    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(new RenderingI18nContext(), Utilities.path("[tmp]", "comparison"), false, "c");
593    TableModel model = gen.new TableModel(id, true);
594    model.setAlternating(true);
595    model.getTitles().add(gen.new Title(null, null, "Item", "The type of item being compared", null, 100));
596    model.getTitles().add(gen.new Title(null, null, "Property", "The system for the concept", null, 100, 2));
597    model.getTitles().add(gen.new Title(null, null, "Value", "The display for the concept", null, 200, 2));
598    model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200));
599    for (StructuralMatch<Element> t : csc.getIncludes().getChildren()) {
600      addComposeRow(gen, model.getRows(), t, "include");
601    }
602    for (StructuralMatch<Element> t : csc.getExcludes().getChildren()) {
603      addComposeRow(gen, model.getRows(), t, "exclude");
604    }
605    return gen.generate(model, prefix, 0, null);
606  }
607
608  private void addComposeRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<Element> t, String name) {
609    Row r = gen.new Row();
610    rows.add(r);
611    r.getCells().add(gen.new Cell(null, null, name, null, null));
612    if (t.hasLeft() && t.hasRight()) {
613      ConceptSetComponent csL = (ConceptSetComponent) t.getLeft();
614      ConceptSetComponent csR = (ConceptSetComponent) t.getRight();
615      if (csL.hasSystem() && csL.getSystem().equals(csR.getSystem())) {
616        r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).span(2).center());        
617      } else {
618        r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
619        r.getCells().add(gen.new Cell(null, null, csR.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
620      }
621      
622      if (csL.hasVersion() && csR.hasVersion()) {
623        if (csL.getVersion().equals(csR.getVersion())) {
624          r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).span(2).center());        
625        } else {
626          r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
627          r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
628        }
629      } else if (csL.hasVersion()) {
630        r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null));        
631        r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT));        
632      } else if (csR.hasVersion()) {        
633        r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT));        
634        r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null));        
635      } else {
636        r.getCells().add(missingCell(gen).span(2).center());
637      }
638
639    } else if (t.hasLeft()) {
640      r.setColor(COLOR_NO_ROW_RIGHT);
641      ConceptSetComponent cs = (ConceptSetComponent) t.getLeft();
642      r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null));
643      r.getCells().add(missingCell(gen));
644      r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null));
645      r.getCells().add(missingCell(gen));
646    } else {
647      r.setColor(COLOR_NO_ROW_LEFT);
648      ConceptSetComponent cs = (ConceptSetComponent) t.getRight();
649      r.getCells().add(missingCell(gen));
650      r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null));
651      r.getCells().add(missingCell(gen));
652      r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null));
653    }
654    r.getCells().add(cellForMessages(gen, t.getMessages()));
655    for (StructuralMatch<Element> c : t.getChildren()) {
656      if (c.either() instanceof ConceptReferenceComponent) {
657        addSetConceptRow(gen, r.getSubRows(), c);
658      } else {
659        addSetFilterRow(gen, r.getSubRows(), c);
660      }
661    }
662  }
663  
664  private void addSetConceptRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<Element> t) {
665    Row r = gen.new Row();
666    rows.add(r);
667    r.getCells().add(gen.new Cell(null, null, "Concept", null, null));
668    if (t.hasLeft() && t.hasRight()) {
669      ConceptReferenceComponent csL = (ConceptReferenceComponent) t.getLeft();
670      ConceptReferenceComponent csR = (ConceptReferenceComponent) t.getRight();
671      // we assume both have codes 
672      if (csL.getCode().equals(csR.getCode())) {
673        r.getCells().add(gen.new Cell(null, null, csL.getCode(), null, null).span(2).center());        
674      } else {
675        r.getCells().add(gen.new Cell(null, null, csL.getCode(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
676        r.getCells().add(gen.new Cell(null, null, csR.getCode(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
677      }
678      
679      if (csL.hasDisplay() && csR.hasDisplay()) {
680        if (csL.getDisplay().equals(csR.getDisplay())) {
681          r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null).span(2).center());        
682        } else {
683          r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
684          r.getCells().add(gen.new Cell(null, null, csR.getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
685        }
686      } else if (csL.hasDisplay()) {
687        r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null));        
688        r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT));        
689      } else if (csR.hasDisplay()) {        
690        r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT));        
691        r.getCells().add(gen.new Cell(null, null, csR.getDisplay(), null, null));        
692      } else {
693        r.getCells().add(missingCell(gen).span(2).center());
694      }
695
696    } else if (t.hasLeft()) {
697      r.setColor(COLOR_NO_ROW_RIGHT);
698      ConceptReferenceComponent cs = (ConceptReferenceComponent) t.getLeft();
699      r.getCells().add(gen.new Cell(null, null, cs.getCode(), null, null));
700      r.getCells().add(missingCell(gen));
701      r.getCells().add(gen.new Cell(null, null, cs.hasDisplay() ? "Version: "+cs.getDisplay() : "", null, null));
702      r.getCells().add(missingCell(gen));
703    } else {
704      r.setColor(COLOR_NO_ROW_LEFT);
705      ConceptReferenceComponent cs = (ConceptReferenceComponent) t.getRight();
706      r.getCells().add(missingCell(gen));
707      r.getCells().add(gen.new Cell(null, null, cs.getCode(), null, null));
708      r.getCells().add(missingCell(gen));
709      r.getCells().add(gen.new Cell(null, null, cs.hasDisplay() ? "Version: "+cs.getDisplay() : "", null, null));
710    }
711    r.getCells().add(cellForMessages(gen, t.getMessages()));
712
713  }
714  
715  private void addSetFilterRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<Element> t) {
716//    Row r = gen.new Row();
717//    rows.add(r);
718//    r.getCells().add(gen.new Cell(null, null, "Filter", null, null));
719//    if (t.hasLeft() && t.hasRight()) {
720//      ConceptSetComponent csL = (ConceptSetComponent) t.getLeft();
721//      ConceptSetComponent csR = (ConceptSetComponent) t.getRight();
722//      // we assume both have systems 
723//      if (csL.getSystem().equals(csR.getSystem())) {
724//        r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).span(2).center());        
725//      } else {
726//        r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
727//        r.getCells().add(gen.new Cell(null, null, csR.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
728//      }
729//      
730//      if (csL.hasVersion() && csR.hasVersion()) {
731//        if (csL.getVersion().equals(csR.getVersion())) {
732//          r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).span(2).center());        
733//        } else {
734//          r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
735//          r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
736//        }
737//      } else if (csL.hasVersion()) {
738//        r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null));        
739//        r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT));        
740//      } else if (csR.hasVersion()) {        
741//        r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT));        
742//        r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null));        
743//      } else {
744//        r.getCells().add(missingCell(gen).span(2).center());
745//      }
746//
747//    } else if (t.hasLeft()) {
748//      r.setColor(COLOR_NO_ROW_RIGHT);
749//      ConceptSetComponent cs = (ConceptSetComponent) t.getLeft();
750//      r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null));
751//      r.getCells().add(missingCell(gen));
752//      r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null));
753//      r.getCells().add(missingCell(gen));
754//    } else {
755//      r.setColor(COLOR_NO_ROW_LEFT);
756//      ConceptSetComponent cs = (ConceptSetComponent) t.getRight();
757//      r.getCells().add(missingCell(gen));
758//      r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null));
759//      r.getCells().add(missingCell(gen));
760//      r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null));
761//    }
762//    r.getCells().add(gen.new Cell(null, null, t.getError(), null, null));
763
764  }
765  
766  public XhtmlNode renderExpansion(ValueSetComparison csc, String id, String prefix) throws IOException {
767    if (csc.getExpansion() == null) {
768      XhtmlNode p = new XhtmlNode(NodeType.Element, "p");
769      p.tx("Unable to generate expansion - see errors");
770      return p;
771    }
772    if (csc.getExpansion().getChildren().isEmpty()) {
773      XhtmlNode p = new XhtmlNode(NodeType.Element, "p");
774      p.tx("Expansion is empty");
775      return p;      
776    }
777    // columns: code(+system), version, display , abstract, inactive,
778    boolean hasSystem = csc.getExpansion().getChildren().isEmpty() ? false : getSystemVaries(csc.getExpansion(), csc.getExpansion().getChildren().get(0).either().getSystem());
779    boolean hasVersion = findVersion(csc.getExpansion());
780    boolean hasAbstract = findAbstract(csc.getExpansion());
781    boolean hasInactive = findInactive(csc.getExpansion());
782
783    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(new RenderingI18nContext(), Utilities.path("[tmp]", "comparison"), false, "c");
784    TableModel model = gen.new TableModel(id, true);
785    model.setAlternating(true);
786    if (hasSystem) {
787      model.getTitles().add(gen.new Title(null, null, "System", "The code for the concept", null, 100));
788    }
789    model.getTitles().add(gen.new Title(null, null, "Code", "The system for the concept", null, 100));
790    model.getTitles().add(gen.new Title(null, null, "Display", "The display for the concept", null, 200, 2));
791//    if (hasVersion) {
792//      model.getTitles().add(gen.new Title(null, null, "Version", "The version for the concept", null, 200, 2));
793//    }
794//    if (hasAbstract) {
795//      model.getTitles().add(gen.new Title(null, null, "Abstract", "The abstract flag for the concept", null, 200, 2));
796//    }
797//    if (hasInactive) {
798//      model.getTitles().add(gen.new Title(null, null, "Inactive", "The inactive flag for the concept", null, 200, 2));
799//    }
800    model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200));
801    for (StructuralMatch<ValueSetExpansionContainsComponent> t : csc.getExpansion().getChildren()) {
802      addExpansionRow(gen, model.getRows(), t, hasSystem, hasVersion, hasAbstract, hasInactive);
803    }
804    return gen.generate(model, prefix, 0, null);
805  }
806
807  private void addExpansionRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<ValueSetExpansionContainsComponent> t, boolean hasSystem, boolean hasVersion, boolean hasAbstract, boolean hasInactive) {
808    Row r = gen.new Row();
809    rows.add(r);
810    if (hasSystem) {
811      r.getCells().add(gen.new Cell(null, null, t.either().getSystem(), null, null));
812    }
813    r.getCells().add(gen.new Cell(null, null, t.either().getCode(), null, null));
814    if (t.hasLeft() && t.hasRight()) {
815      if (t.getLeft().hasDisplay() && t.getRight().hasDisplay()) {
816        if (t.getLeft().getDisplay().equals(t.getRight().getDisplay())) {
817          r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2).center());        
818        } else {
819          r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
820          r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
821        }
822      } else if (t.getLeft().hasDisplay()) {
823        r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null));        
824        r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT));        
825      } else if (t.getRight().hasDisplay()) {        
826        r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT));        
827        r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null));        
828      } else {
829        r.getCells().add(missingCell(gen).span(2).center());
830      }
831
832    } else if (t.hasLeft()) {
833      r.setColor(COLOR_NO_ROW_RIGHT);
834      r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null));
835      r.getCells().add(missingCell(gen));
836    } else {
837      r.setColor(COLOR_NO_ROW_LEFT);
838      r.getCells().add(missingCell(gen));
839      r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null));
840    }
841    r.getCells().add(cellForMessages(gen, t.getMessages()));
842    for (StructuralMatch<ValueSetExpansionContainsComponent> c : t.getChildren()) {
843      addExpansionRow(gen, r.getSubRows(), c, hasSystem, hasVersion, hasAbstract, hasInactive);
844    }
845  }
846
847  private boolean getSystemVaries(StructuralMatch<ValueSetExpansionContainsComponent> list, String system) {
848    for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) {
849      if (t.hasLeft() && !system.equals(t.getLeft().getSystem())) {
850        return true;
851      }
852      if (t.hasRight() && !system.equals(t.getRight().getSystem())) {
853        return true;
854      }
855      if (getSystemVaries(t, system)) {
856        return true;
857      }
858    }
859    return false;
860  }
861
862  private boolean findInactive(StructuralMatch<ValueSetExpansionContainsComponent> list) {
863    for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) {
864      if (t.hasLeft() && t.getLeft().getInactive()) {
865        return true;
866      }
867      if (t.hasRight() && t.getRight().getInactive()) {
868        return true;
869      }
870      if (findInactive(t)) {
871        return true;
872      }
873    }
874    return false;
875  }
876
877  private boolean findAbstract(StructuralMatch<ValueSetExpansionContainsComponent> list) {
878    for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) {
879      if (t.hasLeft() && t.getLeft().getAbstract()) {
880        return true;
881      }
882      if (t.hasRight() && t.getRight().getAbstract()) {
883        return true;
884      }
885      if (findAbstract(t)) {
886        return true;
887      }
888    }
889    return false;
890  }
891
892  private boolean findVersion(StructuralMatch<ValueSetExpansionContainsComponent> list) {
893    for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) {
894      if (t.hasLeft() && t.getLeft().hasVersion()) {
895        return true;
896      }
897      if (t.hasRight() && t.getRight().hasVersion()) {
898        return true;
899      }
900      if (findVersion(t)) {
901        return true;
902      }
903    }
904    return false;
905  }
906
907}