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