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