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