001package org.hl7.fhir.r5.comparison;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010
011import org.hl7.fhir.exceptions.FHIRException;
012import org.hl7.fhir.r5.model.Base;
013import org.hl7.fhir.r5.model.CanonicalResource;
014import org.hl7.fhir.r5.model.CanonicalType;
015import org.hl7.fhir.r5.model.CodeType;
016import org.hl7.fhir.r5.model.CodeableConcept;
017import org.hl7.fhir.r5.model.Coding;
018import org.hl7.fhir.r5.model.DataType;
019import org.hl7.fhir.r5.model.PrimitiveType;
020import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
021import org.hl7.fhir.utilities.Utilities;
022import org.hl7.fhir.utilities.i18n.RenderingI18nContext;
023import org.hl7.fhir.utilities.validation.ValidationMessage;
024import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
025import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
026import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
027import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
028import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
029import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
030import org.hl7.fhir.utilities.xhtml.XhtmlNode;
031
032public abstract class CanonicalResourceComparer extends ResourceComparer {
033
034
035  public enum ChangeAnalysisState {
036    Unknown, NotChanged, Changed, CannotEvaluate;
037
038    boolean noteable() {
039      return this == Changed || this == CannotEvaluate;
040    }
041  }
042
043
044  public abstract class CanonicalResourceComparison<T extends CanonicalResource> extends ResourceComparison {
045    protected T left;
046    protected T right;
047    protected T union;
048    protected T intersection;
049    
050    private ChangeAnalysisState changedMetadata = ChangeAnalysisState.Unknown; 
051    private ChangeAnalysisState changedDefinitions = ChangeAnalysisState.Unknown;
052    private ChangeAnalysisState changedContent = ChangeAnalysisState.Unknown;
053    private ChangeAnalysisState changedContentInterpretation = ChangeAnalysisState.Unknown;
054
055    protected Map<String, StructuralMatch<String>> metadata = new HashMap<>();
056    private List<String> chMetadataFields;                                             
057
058    public CanonicalResourceComparison(T left, T right) {
059      super(left.getId(), right.getId());
060      this.left = left;
061      this.right = right;
062    }
063
064    public T getLeft() {
065      return left;
066    }
067
068    public T getRight() {
069      return right;
070    }
071
072    public T getUnion() {
073      return union;
074    }
075
076    public T getIntersection() {
077      return intersection;
078    }
079
080    public Map<String, StructuralMatch<String>> getMetadata() {
081      return metadata;
082    }
083
084    public void setLeft(T left) {
085      this.left = left;
086    }
087
088    public void setRight(T right) {
089      this.right = right;
090    }
091
092    public void setUnion(T union) {
093      this.union = union;
094    }
095
096    public void setIntersection(T intersection) {
097      this.intersection = intersection;
098    }
099
100    private ChangeAnalysisState updateState(ChangeAnalysisState newState, ChangeAnalysisState oldState) {
101      switch (newState) {
102      case CannotEvaluate:
103        return ChangeAnalysisState.CannotEvaluate;
104      case Changed:
105        if (oldState != ChangeAnalysisState.CannotEvaluate) {
106          return ChangeAnalysisState.Changed;
107        }
108        break;
109      case NotChanged:
110        if (oldState == ChangeAnalysisState.Unknown) {
111          return ChangeAnalysisState.NotChanged;
112        }
113        break;
114      case Unknown:
115      default:
116        break;
117      }
118      return oldState;
119    }
120    
121    public void updatedMetadataState(ChangeAnalysisState state) {
122      changedMetadata = updateState(state, changedMetadata);
123    }
124
125    public void updateDefinitionsState(ChangeAnalysisState state) {
126      changedDefinitions = updateState(state, changedDefinitions);
127    }
128
129    public void updateContentState(ChangeAnalysisState state) {
130      changedContent = updateState(state, changedContent);
131    }
132
133    public void updateContentInterpretationState(ChangeAnalysisState state) {
134      changedContentInterpretation = updateState(state, changedContentInterpretation);
135    }
136
137    public void updatedMetadataState(boolean changed, List<String> chMetadataFields) {
138      changedMetadata = updateState(changed ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedMetadata);
139      this.chMetadataFields = chMetadataFields;
140    }
141
142    public void updateDefinitionsState(boolean changed) {
143      changedDefinitions = updateState(changed ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedDefinitions);
144    }
145
146    public void updateContentState(boolean changed) {
147      changedContent = updateState(changed ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedContent);
148    }
149
150    public void updateContentInterpretationState(boolean changed) {
151      changedContentInterpretation = updateState(changed ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedContentInterpretation);
152    }
153
154    public boolean anyUpdates() {
155      return changedMetadata.noteable() || changedDefinitions.noteable() || changedContent.noteable() || changedContentInterpretation.noteable();
156    }
157    
158    
159    public ChangeAnalysisState getChangedMetadata() {
160      return changedMetadata;
161    }
162
163    public ChangeAnalysisState getChangedDefinitions() {
164      return changedDefinitions;
165    }
166
167    public ChangeAnalysisState getChangedContent() {
168      return changedContent;
169    }
170
171    public ChangeAnalysisState getChangedContentInterpretation() {
172      return changedContentInterpretation;
173    }
174
175    @Override
176    protected String toTable() {
177      String s = "";
178      s = s + refCell(left);
179      s = s + refCell(right);
180      s = s + "<td><a href=\""+getId()+".html\">Comparison</a></td>";
181      s = s + "<td><a href=\""+getId()+"-union.html\">Union</a></td>";
182      s = s + "<td><a href=\""+getId()+"-intersection.html\">Intersection</a></td>";
183      s = s + "<td>"+outcomeSummary()+"</td>";
184      return "<tr style=\"background-color: "+color()+"\">"+s+"</tr>\r\n";
185    }
186
187    @Override
188    protected void countMessages(MessageCounts cnts) {
189      for (StructuralMatch<String> sm : metadata.values()) {
190        sm.countMessages(cnts);
191      }
192    }
193    
194    protected String changeSummary() {
195      if (!(changedMetadata.noteable() || changedDefinitions.noteable() || changedContent.noteable() || changedContentInterpretation.noteable())) {
196        return null;
197      };
198      CommaSeparatedStringBuilder bc = new CommaSeparatedStringBuilder();
199      if (changedMetadata == ChangeAnalysisState.CannotEvaluate) {
200        bc.append("Metadata");
201      }
202      if (changedDefinitions == ChangeAnalysisState.CannotEvaluate) {
203        bc.append("Definitions");
204      }
205      if (changedContent == ChangeAnalysisState.CannotEvaluate) {
206        bc.append("Content");
207      }
208      if (changedContentInterpretation == ChangeAnalysisState.CannotEvaluate) {
209        bc.append("Interpretation");
210      }
211      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
212      if (changedMetadata == ChangeAnalysisState.Changed) {
213        b.append("Metadata");
214      }
215      if (changedDefinitions == ChangeAnalysisState.Changed) {
216        b.append("Definitions");
217      }
218      if (changedContent == ChangeAnalysisState.Changed) {
219        b.append("Content");
220      }
221      if (changedContentInterpretation == ChangeAnalysisState.Changed) {
222        b.append("Interpretation");
223      }
224      return (bc.length() == 0 ? "" : "Error Checking: "+bc.toString()+"; ")+ "Changed: "+b.toString();     
225    }
226
227    public String getMetadataFieldsAsText() {
228      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
229      if (chMetadataFields != null) {
230        for (String s : chMetadataFields) {
231          b.append(s);
232        }
233      }
234      return b.toString();
235    }
236
237    public boolean noUpdates() {
238      return !(changedMetadata.noteable() || changedDefinitions.noteable() || !changedContent.noteable() || !changedContentInterpretation.noteable());
239    }
240
241    public boolean noChangeOtherThanMetadata(String[] metadataFields) {
242      if (changedDefinitions.noteable() || changedContent.noteable() || changedContentInterpretation.noteable()) {
243        return false;
244      }
245      if (!changedMetadata.noteable()) {
246        return true;
247      }
248      for (String s : this.chMetadataFields) {
249        if (!Utilities.existsInList(s, metadataFields)) {
250          return false;
251        }
252      }
253      return true;
254    }
255  }
256
257  public CanonicalResourceComparer(ComparisonSession session) {
258    super(session);
259  }
260
261  protected boolean compareMetadata(CanonicalResource left, CanonicalResource right, Map<String, StructuralMatch<String>> comp, CanonicalResourceComparison<? extends CanonicalResource> res, List<String> changes, Base parent) {
262    var changed = false;
263    if (comparePrimitivesWithTracking("url", left.getUrlElement(), right.getUrlElement(), comp, IssueSeverity.ERROR, res, parent)) {
264      changed = true;
265      changes.add("url");
266    }
267    if (!session.isAnnotate()) {
268      if (comparePrimitivesWithTracking("version", left.getVersionElement(), right.getVersionElement(), comp, IssueSeverity.ERROR, res, parent)) {
269        changed = true;
270        changes.add("version");
271      }
272    }
273    if (comparePrimitivesWithTracking("name", left.getNameElement(), right.getNameElement(), comp, IssueSeverity.INFORMATION, res, parent)) {
274      changed = true;
275      changes.add("name");
276    }
277    if (comparePrimitivesWithTracking("title", left.getTitleElement(), right.getTitleElement(), comp, IssueSeverity.INFORMATION, res, parent)) {
278      changed = true;
279      changes.add("title");
280    }
281    if (comparePrimitivesWithTracking("status", left.getStatusElement(), right.getStatusElement(), comp, IssueSeverity.INFORMATION, res, parent)) {
282      changed = true;
283      changes.add("status");
284    }
285    if (comparePrimitivesWithTracking("experimental", left.getExperimentalElement(), right.getExperimentalElement(), comp, IssueSeverity.WARNING, res, parent)) {
286      changed = true;
287      changes.add("experimental");
288    }
289    if (!session.isAnnotate()) {
290      if (comparePrimitivesWithTracking("date", left.getDateElement(), right.getDateElement(), comp, IssueSeverity.INFORMATION, res, parent)) {
291        changed = true;
292        changes.add("date");
293      }
294    }
295    if (comparePrimitivesWithTracking("publisher", left.getPublisherElement(), right.getPublisherElement(), comp, IssueSeverity.INFORMATION, res, parent)) {
296      changed = true;
297      changes.add("publisher");
298    }
299    if (comparePrimitivesWithTracking("description", left.getDescriptionElement(), right.getDescriptionElement(), comp, IssueSeverity.NULL, res, parent)) {
300      changed = true;
301      changes.add("description");
302    }
303    if (comparePrimitivesWithTracking("purpose", left.getPurposeElement(), right.getPurposeElement(), comp, IssueSeverity.NULL, res, parent)) {
304      changed = true;
305      changes.add("purpose");
306    }
307    if (comparePrimitivesWithTracking("copyright", left.getCopyrightElement(), right.getCopyrightElement(), comp, IssueSeverity.INFORMATION, res, parent)) {
308      changed = true;
309      changes.add("copyright");
310    }
311    if (compareCodeableConceptList("jurisdiction", left.getJurisdiction(), right.getJurisdiction(), comp, IssueSeverity.INFORMATION, res, res.getUnion().getJurisdiction(), res.getIntersection().getJurisdiction())) {
312      changed = true;
313      changes.add("jurisdiction");
314    }
315    return changed;
316  }
317
318  protected boolean compareCodeableConceptList(String name, List<CodeableConcept> left, List<CodeableConcept> right, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, List<CodeableConcept> union, List<CodeableConcept> intersection ) {
319    boolean result = false;
320    List<CodeableConcept> matchR = new ArrayList<>();
321    StructuralMatch<String> combined = new StructuralMatch<String>();
322    for (CodeableConcept l : left) {
323      CodeableConcept r = findCodeableConceptInList(right, l);
324      if (r == null) {
325        union.add(l);
326        result = true;
327        combined.getChildren().add(new StructuralMatch<String>(gen(l), vm(IssueSeverity.INFORMATION, "Removed the item '"+gen(l)+"'", fhirType()+"."+name, res.getMessages())));
328      } else {
329        matchR.add(r);
330        union.add(r);
331        intersection.add(r);
332        StructuralMatch<String> sm = new StructuralMatch<String>(gen(l), gen(r));
333        combined.getChildren().add(sm);
334        if (sm.isDifferent()) {
335          result = true;
336        }
337      }
338    }
339    for (CodeableConcept r : right) {
340      if (!matchR.contains(r)) {
341        union.add(r);
342        result = true;
343        combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+gen(r)+"'", fhirType()+"."+name, res.getMessages()), gen(r)));        
344      }
345    }    
346    comp.put(name, combined);  
347    return result;
348  }
349  
350
351  private CodeableConcept findCodeableConceptInList(List<CodeableConcept> list, CodeableConcept item) {
352    for (CodeableConcept t : list) {
353      if (t.matches(item)) {
354        return t;
355      }
356    }
357    return null;
358  }
359  
360  protected String gen(CodeableConcept cc) {
361    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
362    for (Coding c : cc.getCoding()) {
363      b.append(gen(c));
364    }
365    return b.toString();
366  }
367
368  protected String gen(Coding c) {
369    return c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode();
370  }
371
372  protected void compareCanonicalList(String name, List<CanonicalType> left, List<CanonicalType> right, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, List<CanonicalType> union, List<CanonicalType> intersection ) {
373    List<CanonicalType> matchR = new ArrayList<>();
374    StructuralMatch<String> combined = new StructuralMatch<String>();
375    for (CanonicalType l : left) {
376      CanonicalType r = findCanonicalInList(right, l);
377      if (r == null) {
378        union.add(l);
379        combined.getChildren().add(new StructuralMatch<String>(l.getValue(), vm(IssueSeverity.INFORMATION, "Removed the item '"+l.getValue()+"'", fhirType()+"."+name, res.getMessages())));
380      } else {
381        matchR.add(r);
382        union.add(r);
383        intersection.add(r);
384        StructuralMatch<String> sm = new StructuralMatch<String>(l.getValue(), r.getValue());
385        combined.getChildren().add(sm);
386      }
387    }
388    for (CanonicalType r : right) {
389      if (!matchR.contains(r)) {
390        union.add(r);
391        combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+r.getValue()+"'", fhirType()+"."+name, res.getMessages()), r.getValue()));        
392      }
393    }    
394    comp.put(name, combined);    
395  }
396  
397  private CanonicalType findCanonicalInList(List<CanonicalType> list, CanonicalType item) {
398    for (CanonicalType t : list) {
399      if (t.getValue().equals(item.getValue())) {
400        return t;
401      }
402    }
403    return null;
404  }
405
406  protected void compareCodeList(String name, List<CodeType> left, List<CodeType> right, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, List<CodeType> union, List<CodeType> intersection ) {
407    List<CodeType> matchR = new ArrayList<>();
408    StructuralMatch<String> combined = new StructuralMatch<String>();
409    for (CodeType l : left) {
410      CodeType r = findCodeInList(right, l);
411      if (r == null) {
412        union.add(l);
413        combined.getChildren().add(new StructuralMatch<String>(l.getValue(), vm(IssueSeverity.INFORMATION, "Removed the item '"+l.getValue()+"'", fhirType()+"."+name, res.getMessages())));
414      } else {
415        matchR.add(r);
416        union.add(r);
417        intersection.add(r);
418        StructuralMatch<String> sm = new StructuralMatch<String>(l.getValue(), r.getValue());
419        combined.getChildren().add(sm);
420      }
421    }
422    for (CodeType r : right) {
423      if (!matchR.contains(r)) {
424        union.add(r);
425        combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+r.getValue()+"'", fhirType()+"."+name, res.getMessages()), r.getValue()));        
426      }
427    }    
428    comp.put(name, combined);    
429  }
430  
431  private CodeType findCodeInList(List<CodeType> list, CodeType item) {
432    for (CodeType t : list) {
433      if (t.getValue().equals(item.getValue())) {
434        return t;
435      }
436    }
437    return null;
438  }
439
440  @SuppressWarnings("rawtypes")
441  protected boolean comparePrimitives(String name, PrimitiveType l, PrimitiveType r, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res) {
442    StructuralMatch<String> match = null;
443    if (l.isEmpty() && r.isEmpty()) {
444      match = new StructuralMatch<>(null, null, null);
445    } else if (l.isEmpty()) {
446      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "Added the item '"+r.primitiveValue()+"'", fhirType()+"."+name));
447    } else if (r.isEmpty()) {
448      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "Removed the item '"+l.primitiveValue()+"'", fhirType()+"."+name));
449    } else if (!l.hasValue() && !r.hasValue()) {
450      match = new StructuralMatch<>(null, null, vmI(IssueSeverity.INFORMATION, "No Value", fhirType()+"."+name));
451    } else if (!l.hasValue()) {
452      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "No Value on Left", fhirType()+"."+name));
453    } else if (!r.hasValue()) {
454      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "No Value on Right", fhirType()+"."+name));
455    } else if (l.getValue().equals(r.getValue())) {
456      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), null);
457    } else {
458      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vmI(level, "Values Differ", fhirType()+"."+name));
459      if (level != IssueSeverity.NULL) {
460        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level));
461      }
462    } 
463    comp.put(name, match);  
464    return match.isDifferent();
465  }
466
467
468  protected boolean comparePrimitivesWithTracking(String name, List< ? extends PrimitiveType> ll, List<? extends PrimitiveType> rl, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, Base parent) {
469    boolean def = false;
470    
471    List<PrimitiveType> matchR = new ArrayList<>();
472    for (PrimitiveType l : ll) {
473      PrimitiveType r = findInList(rl, l);
474      if (r == null) {
475        session.markDeleted(parent, "element", l);
476      } else {
477        matchR.add(r);
478        def = comparePrimitivesWithTracking(name, l, r, comp, level, res, parent) || def;
479      }
480    }
481    for (PrimitiveType r : rl) {
482      if (!matchR.contains(r)) {
483        session.markAdded(r);
484      }
485    }
486    return def;    
487  }
488  
489  private PrimitiveType findInList(List<? extends PrimitiveType> rl, PrimitiveType l) {
490    for (PrimitiveType r : rl) {
491      if (r.equalsDeep(l)) {
492        return r;
493      }
494    }
495    return null;
496  }
497
498  @SuppressWarnings("rawtypes")
499  protected boolean comparePrimitivesWithTracking(String name, PrimitiveType l, PrimitiveType r, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, Base parent) {
500    StructuralMatch<String> match = null;
501    if (l.isEmpty() && r.isEmpty()) {
502      match = new StructuralMatch<>(null, null, null);
503    } else if (l.isEmpty()) {
504      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "Added the item '"+r.primitiveValue()+"'", fhirType()+"."+name));
505      session.markAdded(r);
506    } else if (r.isEmpty()) {
507      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "Removed the item '"+l.primitiveValue()+"'", fhirType()+"."+name));
508      session.markDeleted(parent, name, l);
509    } else if (!l.hasValue() && !r.hasValue()) {
510      match = new StructuralMatch<>(null, null, vmI(IssueSeverity.INFORMATION, "No Value", fhirType()+"."+name));
511    } else if (!l.hasValue()) {
512      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "No Value on Left", fhirType()+"."+name));
513      session.markAdded(r);
514    } else if (!r.hasValue()) {
515      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "No Value on Right", fhirType()+"."+name));
516      session.markDeleted(parent, name, l);
517    } else if (l.getValue().equals(r.getValue())) {
518      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), null);
519    } else {
520      session.markChanged(r, l);
521      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vmI(level, "Values Differ", fhirType()+"."+name));
522      if (level != IssueSeverity.NULL && res != null) {
523        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level));
524      }
525    } 
526    if (comp != null) {
527      comp.put(name, match);
528    }
529    return match.isDifferent();
530  }
531  
532
533  protected boolean compareDataTypesWithTracking(String name, List< ? extends DataType> ll, List<? extends DataType> rl, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, Base parent) {
534    boolean def = false;
535    
536    List<DataType> matchR = new ArrayList<>();
537    for (DataType l : ll) {
538      DataType r = findInList(rl, l);
539      if (r == null) {
540        session.markDeleted(parent, "element", l);
541      } else {
542        matchR.add(r);
543        def = compareDataTypesWithTracking(name, l, r, comp, level, res, parent) || def;
544      }
545    }
546    for (DataType r : rl) {
547      if (!matchR.contains(r)) {
548        session.markAdded(r);
549      }
550    }
551    return def;    
552  }
553  
554  private DataType findInList(List<? extends DataType> rl, DataType l) {
555    for (DataType r : rl) {
556      if (r.equalsDeep(l)) {
557        return r;
558      }
559    }
560    return null;
561  }
562
563  @SuppressWarnings("rawtypes")
564  protected boolean compareDataTypesWithTracking(String name, DataType l, DataType r, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, Base parent) {
565    StructuralMatch<String> match = null;
566    boolean le = l == null || l.isEmpty();
567    boolean re = r == null || r.isEmpty(); 
568    if (le && re) {
569      match = new StructuralMatch<>(null, null, null);
570    } else if (le) {
571      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "Added the item '"+r.fhirType()+"'", fhirType()+"."+name));
572      session.markAdded(r);
573    } else if (re) {
574      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "Removed the item '"+l.fhirType()+"'", fhirType()+"."+name));
575      session.markDeleted(parent, name, l);
576    } else if (l.equalsDeep(r)) {
577      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), null);
578    } else {
579      session.markChanged(r, l);
580      match = new StructuralMatch<>(l.fhirType(), r.fhirType(), vmI(level, "Values Differ", fhirType()+"."+name));
581      if (level != IssueSeverity.NULL && res != null) {
582        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.fhirType()+"' vs '"+r.fhirType()+"'", level));
583      }
584    } 
585    if (comp != null) {
586      comp.put(name, match);
587    }
588    return match.isDifferent();
589  }
590
591  protected abstract String fhirType();
592
593  public XhtmlNode renderMetadata(CanonicalResourceComparison<? extends CanonicalResource> comparison, String id, String prefix) throws FHIRException, IOException {
594    // columns: code, display (left|right), properties (left|right)
595    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(new RenderingI18nContext(), Utilities.path("[tmp]", "compare"), false, "c");
596    TableModel model = gen.new TableModel(id, true);
597    model.setAlternating(true);
598    model.getTitles().add(gen.new Title(null, null, "Name", "Property Name", null, 100));
599    model.getTitles().add(gen.new Title(null, null, "Value", "The value of the property", null, 200, 2));
600    model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200));
601
602    for (String n : sorted(comparison.getMetadata().keySet())) {
603      StructuralMatch<String> t = comparison.getMetadata().get(n);
604      addRow(gen, model.getRows(), n, t);
605    }
606    return gen.generate(model, prefix, 0, null);
607  }
608
609  private void addRow(HierarchicalTableGenerator gen, List<Row> rows, String name, StructuralMatch<String> t) {
610    Row r = gen.new Row();
611    rows.add(r);
612    r.getCells().add(gen.new Cell(null, null, name, null, null));
613    if (t.hasLeft() && t.hasRight()) {
614      if (t.getLeft().equals(t.getRight())) {
615        r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).span(2));        
616      } else {
617        r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
618        r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
619      }
620    } else if (t.hasLeft()) {
621      r.setColor(COLOR_NO_ROW_RIGHT);
622      r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null));        
623      r.getCells().add(missingCell(gen));        
624    } else if (t.hasRight()) {        
625      r.setColor(COLOR_NO_ROW_LEFT);
626      r.getCells().add(missingCell(gen));        
627      r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null));        
628    } else {
629      r.getCells().add(missingCell(gen).span(2));
630    }
631    r.getCells().add(cellForMessages(gen, t.getMessages()));
632    int i = 0;
633    for (StructuralMatch<String> c : t.getChildren()) {
634      addRow(gen, r.getSubRows(), name+"["+i+"]", c);
635      i++;
636    }
637  }
638
639
640  private List<String> sorted(Set<String> keys) {
641    List<String> res = new ArrayList<>();
642    res.addAll(keys);
643    Collections.sort(res);
644    return res;
645  }
646
647
648}