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}