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.comparison.ResourceComparer.MessageCounts; 013import org.hl7.fhir.r5.model.CanonicalResource; 014import org.hl7.fhir.r5.model.CanonicalType; 015import org.hl7.fhir.r5.model.CapabilityStatement; 016import org.hl7.fhir.r5.model.CodeType; 017import org.hl7.fhir.r5.model.CodeableConcept; 018import org.hl7.fhir.r5.model.Coding; 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 abstract class CanonicalResourceComparison<T extends CanonicalResource> extends ResourceComparison { 035 protected T left; 036 protected T right; 037 protected T union; 038 protected T intersection; 039 protected Map<String, StructuralMatch<String>> metadata = new HashMap<>(); 040 041 public CanonicalResourceComparison(T left, T right) { 042 super(left.getId(), right.getId()); 043 this.left = left; 044 this.right = right; 045 } 046 047 public T getLeft() { 048 return left; 049 } 050 051 public T getRight() { 052 return right; 053 } 054 055 public T getUnion() { 056 return union; 057 } 058 059 public T getIntersection() { 060 return intersection; 061 } 062 063 public Map<String, StructuralMatch<String>> getMetadata() { 064 return metadata; 065 } 066 067 public void setLeft(T left) { 068 this.left = left; 069 } 070 071 public void setRight(T right) { 072 this.right = right; 073 } 074 075 public void setUnion(T union) { 076 this.union = union; 077 } 078 079 public void setIntersection(T intersection) { 080 this.intersection = intersection; 081 } 082 083 @Override 084 protected String toTable() { 085 String s = ""; 086 s = s + refCell(left); 087 s = s + refCell(right); 088 s = s + "<td><a href=\""+getId()+".html\">Comparison</a></td>"; 089 s = s + "<td>"+outcomeSummary()+"</td>"; 090 return "<tr style=\"background-color: "+color()+"\">"+s+"</tr>\r\n"; 091 } 092 093 @Override 094 protected void countMessages(MessageCounts cnts) { 095 for (StructuralMatch<String> sm : metadata.values()) { 096 sm.countMessages(cnts); 097 } 098 } 099 } 100 101 public CanonicalResourceComparer(ComparisonSession session) { 102 super(session); 103 } 104 105 protected void compareMetadata(CanonicalResource left, CanonicalResource right, Map<String, StructuralMatch<String>> comp, CanonicalResourceComparison<? extends CanonicalResource> res) { 106 comparePrimitives("url", left.getUrlElement(), right.getUrlElement(), comp, IssueSeverity.ERROR, res); 107 comparePrimitives("version", left.getVersionElement(), right.getVersionElement(), comp, IssueSeverity.ERROR, res); 108 comparePrimitives("name", left.getNameElement(), right.getNameElement(), comp, IssueSeverity.INFORMATION, res); 109 comparePrimitives("title", left.getTitleElement(), right.getTitleElement(), comp, IssueSeverity.INFORMATION, res); 110 comparePrimitives("status", left.getStatusElement(), right.getStatusElement(), comp, IssueSeverity.INFORMATION, res); 111 comparePrimitives("experimental", left.getExperimentalElement(), right.getExperimentalElement(), comp, IssueSeverity.WARNING, res); 112 comparePrimitives("date", left.getDateElement(), right.getDateElement(), comp, IssueSeverity.INFORMATION, res); 113 comparePrimitives("publisher", left.getPublisherElement(), right.getPublisherElement(), comp, IssueSeverity.INFORMATION, res); 114 comparePrimitives("description", left.getDescriptionElement(), right.getDescriptionElement(), comp, IssueSeverity.NULL, res); 115 comparePrimitives("purpose", left.getPurposeElement(), right.getPurposeElement(), comp, IssueSeverity.NULL, res); 116 comparePrimitives("copyright", left.getCopyrightElement(), right.getCopyrightElement(), comp, IssueSeverity.INFORMATION, res); 117 compareCodeableConceptList("jurisdiction", left.getJurisdiction(), right.getJurisdiction(), comp, IssueSeverity.INFORMATION, res, res.getUnion().getJurisdiction(), res.getIntersection().getJurisdiction()); 118 } 119 120 protected void 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 ) { 121 List<CodeableConcept> matchR = new ArrayList<>(); 122 StructuralMatch<String> combined = new StructuralMatch<String>(); 123 for (CodeableConcept l : left) { 124 CodeableConcept r = findCodeableConceptInList(right, l); 125 if (r == null) { 126 union.add(l); 127 combined.getChildren().add(new StructuralMatch<String>(gen(l), vm(IssueSeverity.INFORMATION, "Removed the item '"+gen(l)+"'", fhirType()+"."+name, res.getMessages()))); 128 } else { 129 matchR.add(r); 130 union.add(r); 131 intersection.add(r); 132 StructuralMatch<String> sm = new StructuralMatch<String>(gen(l), gen(r)); 133 combined.getChildren().add(sm); 134 } 135 } 136 for (CodeableConcept r : right) { 137 if (!matchR.contains(r)) { 138 union.add(r); 139 combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+gen(r)+"'", fhirType()+"."+name, res.getMessages()), gen(r))); 140 } 141 } 142 comp.put(name, combined); 143 } 144 145 146 private CodeableConcept findCodeableConceptInList(List<CodeableConcept> list, CodeableConcept item) { 147 for (CodeableConcept t : list) { 148 if (t.matches(item)) { 149 return t; 150 } 151 } 152 return null; 153 } 154 155 protected String gen(CodeableConcept cc) { 156 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 157 for (Coding c : cc.getCoding()) { 158 b.append(gen(c)); 159 } 160 return b.toString(); 161 } 162 163 protected String gen(Coding c) { 164 return c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode(); 165 } 166 167 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 ) { 168 List<CanonicalType> matchR = new ArrayList<>(); 169 StructuralMatch<String> combined = new StructuralMatch<String>(); 170 for (CanonicalType l : left) { 171 CanonicalType r = findCanonicalInList(right, l); 172 if (r == null) { 173 union.add(l); 174 combined.getChildren().add(new StructuralMatch<String>(l.getValue(), vm(IssueSeverity.INFORMATION, "Removed the item '"+l.getValue()+"'", fhirType()+"."+name, res.getMessages()))); 175 } else { 176 matchR.add(r); 177 union.add(r); 178 intersection.add(r); 179 StructuralMatch<String> sm = new StructuralMatch<String>(l.getValue(), r.getValue()); 180 combined.getChildren().add(sm); 181 } 182 } 183 for (CanonicalType r : right) { 184 if (!matchR.contains(r)) { 185 union.add(r); 186 combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+r.getValue()+"'", fhirType()+"."+name, res.getMessages()), r.getValue())); 187 } 188 } 189 comp.put(name, combined); 190 } 191 192 private CanonicalType findCanonicalInList(List<CanonicalType> list, CanonicalType item) { 193 for (CanonicalType t : list) { 194 if (t.getValue().equals(item.getValue())) { 195 return t; 196 } 197 } 198 return null; 199 } 200 201 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 ) { 202 List<CodeType> matchR = new ArrayList<>(); 203 StructuralMatch<String> combined = new StructuralMatch<String>(); 204 for (CodeType l : left) { 205 CodeType r = findCodeInList(right, l); 206 if (r == null) { 207 union.add(l); 208 combined.getChildren().add(new StructuralMatch<String>(l.getValue(), vm(IssueSeverity.INFORMATION, "Removed the item '"+l.getValue()+"'", fhirType()+"."+name, res.getMessages()))); 209 } else { 210 matchR.add(r); 211 union.add(r); 212 intersection.add(r); 213 StructuralMatch<String> sm = new StructuralMatch<String>(l.getValue(), r.getValue()); 214 combined.getChildren().add(sm); 215 } 216 } 217 for (CodeType r : right) { 218 if (!matchR.contains(r)) { 219 union.add(r); 220 combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+r.getValue()+"'", fhirType()+"."+name, res.getMessages()), r.getValue())); 221 } 222 } 223 comp.put(name, combined); 224 } 225 226 private CodeType findCodeInList(List<CodeType> list, CodeType item) { 227 for (CodeType t : list) { 228 if (t.getValue().equals(item.getValue())) { 229 return t; 230 } 231 } 232 return null; 233 } 234 235 @SuppressWarnings("rawtypes") 236 protected void comparePrimitives(String name, PrimitiveType l, PrimitiveType r, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res) { 237 StructuralMatch<String> match = null; 238 if (l.isEmpty() && r.isEmpty()) { 239 match = new StructuralMatch<>(null, null, null); 240 } else if (l.isEmpty()) { 241 match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "Added the item '"+r.primitiveValue()+"'", fhirType()+"."+name)); 242 } else if (r.isEmpty()) { 243 match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "Removed the item '"+l.primitiveValue()+"'", fhirType()+"."+name)); 244 } else if (!l.hasValue() && !r.hasValue()) { 245 match = new StructuralMatch<>(null, null, vmI(IssueSeverity.INFORMATION, "No Value", fhirType()+"."+name)); 246 } else if (!l.hasValue()) { 247 match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "No Value on Left", fhirType()+"."+name)); 248 } else if (!r.hasValue()) { 249 match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "No Value on Right", fhirType()+"."+name)); 250 } else if (l.getValue().equals(r.getValue())) { 251 match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), null); 252 } else { 253 match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vmI(level, "Values Differ", fhirType()+"."+name)); 254 if (level != IssueSeverity.NULL) { 255 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level)); 256 } 257 } 258 comp.put(name, match); 259 } 260 261 protected abstract String fhirType(); 262 263 public XhtmlNode renderMetadata(CanonicalResourceComparison<? extends CanonicalResource> comparison, String id, String prefix) throws FHIRException, IOException { 264 // columns: code, display (left|right), properties (left|right) 265 HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "compare"), false); 266 TableModel model = gen.new TableModel(id, true); 267 model.setAlternating(true); 268 model.getTitles().add(gen.new Title(null, null, "Name", "Property Name", null, 100)); 269 model.getTitles().add(gen.new Title(null, null, "Value", "The value of the property", null, 200, 2)); 270 model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); 271 272 for (String n : sorted(comparison.getMetadata().keySet())) { 273 StructuralMatch<String> t = comparison.getMetadata().get(n); 274 addRow(gen, model.getRows(), n, t); 275 } 276 return gen.generate(model, prefix, 0, null); 277 } 278 279 private void addRow(HierarchicalTableGenerator gen, List<Row> rows, String name, StructuralMatch<String> t) { 280 Row r = gen.new Row(); 281 rows.add(r); 282 r.getCells().add(gen.new Cell(null, null, name, null, null)); 283 if (t.hasLeft() && t.hasRight()) { 284 if (t.getLeft().equals(t.getRight())) { 285 r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).span(2)); 286 } else { 287 r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 288 r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 289 } 290 } else if (t.hasLeft()) { 291 r.setColor(COLOR_NO_ROW_RIGHT); 292 r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null)); 293 r.getCells().add(missingCell(gen)); 294 } else if (t.hasRight()) { 295 r.setColor(COLOR_NO_ROW_LEFT); 296 r.getCells().add(missingCell(gen)); 297 r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null)); 298 } else { 299 r.getCells().add(missingCell(gen).span(2)); 300 } 301 r.getCells().add(cellForMessages(gen, t.getMessages())); 302 int i = 0; 303 for (StructuralMatch<String> c : t.getChildren()) { 304 addRow(gen, r.getSubRows(), name+"["+i+"]", c); 305 i++; 306 } 307 } 308 309 310 private List<String> sorted(Set<String> keys) { 311 List<String> res = new ArrayList<>(); 312 res.addAll(keys); 313 Collections.sort(res); 314 return res; 315 } 316 317 318}