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}