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