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