
001package org.hl7.fhir.r5.comparison; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Date; 006import java.util.List; 007 008import org.hl7.fhir.exceptions.DefinitionException; 009import org.hl7.fhir.exceptions.FHIRException; 010import org.hl7.fhir.exceptions.FHIRFormatError; 011import org.hl7.fhir.r5.comparison.CodeSystemComparer.CodeSystemComparison; 012import org.hl7.fhir.r5.context.IWorkerContext; 013import org.hl7.fhir.r5.model.CanonicalType; 014import org.hl7.fhir.r5.model.Element; 015import org.hl7.fhir.r5.model.ValueSet; 016import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 017import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 018import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 019import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; 020import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 021import org.hl7.fhir.r5.renderers.CodeSystemRenderer; 022import org.hl7.fhir.r5.renderers.ValueSetRenderer; 023import org.hl7.fhir.r5.renderers.utils.RenderingContext; 024import org.hl7.fhir.r5.renderers.utils.ResourceWrapper; 025import org.hl7.fhir.r5.renderers.utils.RenderingContext.GenerationRules; 026import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode; 027import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; 028import org.hl7.fhir.r5.utils.EOperationOutcome; 029import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 030import org.hl7.fhir.utilities.Utilities; 031import org.hl7.fhir.utilities.i18n.RenderingI18nContext; 032import org.hl7.fhir.utilities.validation.ValidationMessage; 033import org.hl7.fhir.utilities.validation.ValidationOptions; 034import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 035import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 036import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 037import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; 038import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; 039import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; 040import org.hl7.fhir.utilities.xhtml.NodeType; 041import org.hl7.fhir.utilities.xhtml.XhtmlNode; 042 043@MarkedToMoveToAdjunctPackage 044public class ValueSetComparer extends CanonicalResourceComparer { 045 046 public class ValueSetComparison extends CanonicalResourceComparison<ValueSet> { 047 048 public ValueSetComparison(ValueSet left, ValueSet right) { 049 super(left, right); 050 } 051 052 private StructuralMatch<Element> includes = new StructuralMatch<>(); 053 private StructuralMatch<Element> excludes = new StructuralMatch<>(); 054 private StructuralMatch<ValueSetExpansionContainsComponent> expansion; 055 056 public StructuralMatch<Element> getIncludes() { 057 return includes; 058 } 059 060 public StructuralMatch<Element> getExcludes() { 061 return excludes; 062 } 063 064 public StructuralMatch<ValueSetExpansionContainsComponent> getExpansion() { 065 return expansion; 066 } 067 068 public StructuralMatch<ValueSetExpansionContainsComponent> forceExpansion() { 069 if (expansion == null) { 070 expansion = new StructuralMatch<>(); 071 } 072 return expansion; 073 } 074 075 @Override 076 protected String abbreviation() { 077 return "vs"; 078 } 079 080 @Override 081 protected String summary() { 082 String res = "ValueSet: "+left.present()+" vs "+right.present(); 083 String ch = changeSummary(); 084 if (ch != null) { 085 res = res + ". "+ch; 086 } 087 return res; 088 } 089 090 @Override 091 protected String fhirType() { 092 return "ValueSet"; 093 } 094 @Override 095 protected void countMessages(MessageCounts cnts) { 096 super.countMessages(cnts); 097 if (includes != null) { 098 includes.countMessages(cnts); 099 } 100 if (excludes != null) { 101 excludes.countMessages(cnts); 102 } 103 if (expansion != null) { 104 expansion.countMessages(cnts); 105 } 106 } 107 108 } 109 110 public ValueSetComparer(ComparisonSession session) { 111 super(session); 112 } 113 114 public ValueSetComparison compare(ValueSet left, ValueSet right) { 115 if (left == null) 116 throw new DefinitionException("No ValueSet provided (left)"); 117 if (right == null) 118 throw new DefinitionException("No ValueSet provided (right)"); 119 120 ValueSetComparison res = new ValueSetComparison(left, right); 121 session.identify(res); 122 ValueSet vs = new ValueSet(); 123 res.setUnion(vs); 124 session.identify(vs); 125 vs.setName("Union"+left.getName()+"And"+right.getName()); 126 vs.setTitle("Union of "+left.getTitle()+" And "+right.getTitle()); 127 vs.setStatus(left.getStatus()); 128 vs.setDate(new Date()); 129 130 ValueSet vs1 = new ValueSet(); 131 res.setIntersection(vs1); 132 session.identify(vs1); 133 vs1.setName("Intersection"+left.getName()+"And"+right.getName()); 134 vs1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle()); 135 vs1.setStatus(left.getStatus()); 136 vs1.setDate(new Date()); 137 138 List<String> chMetadata = new ArrayList<>(); 139 var ch = compareMetadata(left, right, res.getMetadata(), res, chMetadata, right); 140 var def = false; 141 if (comparePrimitives("immutable", left.getImmutableElement(), right.getImmutableElement(), res.getMetadata(), IssueSeverity.WARNING, res)) { 142 ch = true; 143 chMetadata.add("immutable"); 144 } 145 if (left.hasCompose() || right.hasCompose()) { 146 if (comparePrimitives("compose.lockedDate", left.getCompose().getLockedDateElement(), right.getCompose().getLockedDateElement(), res.getMetadata(), IssueSeverity.WARNING, res)) { 147 ch = true; 148 chMetadata.add("compose.lockedDate"); 149 } 150 def = comparePrimitives("compose.inactive", left.getCompose().getInactiveElement(), right.getCompose().getInactiveElement(), res.getMetadata(), IssueSeverity.WARNING, res) || def; 151 } 152 res.updatedMetadataState(ch, chMetadata); 153 154 def = compareCompose(left.getCompose(), right.getCompose(), res, res.getUnion().getCompose(), res.getIntersection().getCompose()) || def; 155 res.updateDefinitionsState(def); 156// compareExpansions(left, right, res); 157 session.annotate(right, res); 158 return res; 159 } 160 161 private boolean compareCompose(ValueSetComposeComponent left, ValueSetComposeComponent right, ValueSetComparison res, ValueSetComposeComponent union, ValueSetComposeComponent intersection) { 162 boolean def = false; 163 // first, the includes 164 List<ConceptSetComponent> matchR = new ArrayList<>(); 165 for (ConceptSetComponent l : left.getInclude()) { 166 ConceptSetComponent r = findInList(right.getInclude(), l, left.getInclude()); 167 if (r == null) { 168 union.getInclude().add(l); 169 res.updateContentState(true); 170 res.getIncludes().getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.INFORMATION, "Removed Include", "ValueSet.compose.include"))); 171 session.markDeleted(right, "include", l); 172 } else { 173 matchR.add(r); 174 ConceptSetComponent csM = new ConceptSetComponent(); 175 ConceptSetComponent csI = new ConceptSetComponent(); 176 union.getInclude().add(csM); 177 intersection.getInclude().add(csI); 178 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r); 179 res.getIncludes().getChildren().add(sm); 180 def = compareDefinitions("ValueSet.compose.include["+right.getInclude().indexOf(r)+"]", l, r, sm, csM, csI, res) || def; 181 } 182 } 183 for (ConceptSetComponent r : right.getInclude()) { 184 if (!matchR.contains(r)) { 185 union.getInclude().add(r); 186 res.updateContentState(true); 187 res.getIncludes().getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.INFORMATION, "Added Include", "ValueSet.compose.include"), r)); 188 session.markAdded(r); 189 } 190 } 191 192 // now. the excludes 193 matchR.clear(); 194 for (ConceptSetComponent l : left.getExclude()) { 195 ConceptSetComponent r = findInList(right.getExclude(), l, left.getExclude()); 196 if (r == null) { 197 union.getExclude().add(l); 198 res.updateContentState(true); 199 res.getExcludes().getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.INFORMATION, "Removed Exclude", "ValueSet.compose.exclude"))); 200 } else { 201 matchR.add(r); 202 ConceptSetComponent csM = new ConceptSetComponent(); 203 ConceptSetComponent csI = new ConceptSetComponent(); 204 union.getExclude().add(csM); 205 intersection.getExclude().add(csI); 206 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r); 207 res.getExcludes().getChildren().add(sm); 208 def = compareDefinitions("ValueSet.compose.exclude["+right.getExclude().indexOf(r)+"]", l, r, sm, csM, csI, res) || def; 209 } 210 } 211 for (ConceptSetComponent r : right.getExclude()) { 212 if (!matchR.contains(r)) { 213 union.getExclude().add(r); 214 res.updateContentState(true); 215 res.getExcludes().getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.INFORMATION, "Added Exclude", "ValueSet.compose.exclude"), r)); 216 } 217 } 218 return def; 219 } 220 221 private ConceptSetComponent findInList(List<ConceptSetComponent> matches, ConceptSetComponent item, List<ConceptSetComponent> source) { 222 if (matches.size() == 1 && source.size() == 1) { 223 return matches.get(0); 224 } 225 int matchCount = countMatchesBySystem(matches, item); 226 int sourceCount = countMatchesBySystem(source, item); 227 228 if (matchCount == 1 && sourceCount == 1) { 229 for (ConceptSetComponent t : matches) { 230 if (t.getSystem() != null && t.getSystem().equals(item.getSystem())) { 231 return t; 232 } 233 } 234 } 235 // if there's more than one candidate match by system, then we look for a full match 236 for (ConceptSetComponent t : matches) { 237 if (t.equalsDeep(item)) { 238 return t; 239 } 240 } 241 return null; 242 } 243 244 private int countMatchesBySystem(List<ConceptSetComponent> list, ConceptSetComponent item) { 245 int c = 0; 246 for (ConceptSetComponent t : list) { 247 if (t.hasSystem() && t.getSystem().equals(item.getSystem())) { 248 c++; 249 } 250 } 251 return c; 252 } 253 254 255 private boolean compareDefinitions(String path, ConceptSetComponent left, ConceptSetComponent right, StructuralMatch<Element> combined, ConceptSetComponent union, ConceptSetComponent intersection, ValueSetComparison res) { 256 boolean def = false; 257 // system must match, but the rest might not. we're going to do the full comparison whatever, so the outcome looks consistent to the user 258 List<CanonicalType> matchVSR = new ArrayList<>(); 259 for (CanonicalType l : left.getValueSet()) { 260 CanonicalType r = findInList(right.getValueSet(), l, left.getValueSet()); 261 if (r == null) { 262 union.getValueSet().add(l); 263 res.updateContentState(true); 264 combined.getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.ERROR, "Removed ValueSet", "ValueSet.compose.include.valueSet"))); 265 if (session.isAnnotate()) { 266 session.markDeleted(right, "valueset", l); 267 } 268 } else { 269 matchVSR.add(r); 270 if (l.getValue().equals(r.getValue())) { 271 union.getValueSet().add(l); 272 intersection.getValueSet().add(l); 273 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, null); 274 combined.getChildren().add(sm); 275 } else { 276 // it's not possible to get here? 277 union.getValueSet().add(l); 278 union.getValueSet().add(r); 279 res.updateContentState(true); 280 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, vmI(IssueSeverity.WARNING, "Values are different", "ValueSet.compose.include.valueSet")); 281 combined.getChildren().add(sm); 282 if (session.isAnnotate()) { 283 session.markChanged(r, l); 284 } 285 286 } 287 } 288 } 289 for (CanonicalType r : right.getValueSet()) { 290 if (!matchVSR.contains(r)) { 291 union.getValueSet().add(r); 292 res.updateContentState(true); 293 combined.getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.ERROR, "Add ValueSet", "ValueSet.compose.include.valueSet"), r)); 294 session.markAdded(r); 295 } 296 } 297 298 List<ConceptReferenceComponent> matchCR = new ArrayList<>(); 299 for (ConceptReferenceComponent l : left.getConcept()) { 300 ConceptReferenceComponent r = findInList(right.getConcept(), l, left.getConcept()); 301 if (r == null) { 302 union.getConcept().add(l); 303 res.updateContentState(true); 304 combined.getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.ERROR, "Removed this Concept", "ValueSet.compose.include.concept"))); 305 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" removed", IssueSeverity.ERROR)); 306 session.markDeleted(right,"concept", l); 307 } else { 308 matchCR.add(r); 309 if (l.getCode().equals(r.getCode())) { 310 ConceptReferenceComponent cu = new ConceptReferenceComponent(); 311 ConceptReferenceComponent ci = new ConceptReferenceComponent(); 312 union.getConcept().add(cu); 313 intersection.getConcept().add(ci); 314 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r); 315 combined.getChildren().add(sm); 316 def = compareConcepts(path+".concept["+right.getConcept().indexOf(r)+"]", l, r, sm, cu, ci, res) || def; 317 } else { 318 // not that it's possible to get here? 319 union.getConcept().add(l); 320 union.getConcept().add(r); 321 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, vmI(IssueSeverity.WARNING, "Concepts are different", "ValueSet.compose.include.concept")); 322 combined.getChildren().add(sm); 323 res.updateContentState(true); 324 compareConcepts(path+".concept["+right.getConcept().indexOf(r)+"]", l, r, sm, null, null, res); 325 session.markChanged(r, l); 326 } 327 } 328 } 329 for (ConceptReferenceComponent r : right.getConcept()) { 330 if (!matchCR.contains(r)) { 331 union.getConcept().add(r); 332 res.updateContentState(true); 333 combined.getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.ERROR, "Added this Concept", "ValueSet.compose.include.concept"), r)); 334 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+r.getCode()+" added", IssueSeverity.ERROR)); 335 session.markAdded(r); 336 } 337 } 338 339 List<ConceptSetFilterComponent> matchFR = new ArrayList<>(); 340 for (ConceptSetFilterComponent l : left.getFilter()) { 341 ConceptSetFilterComponent r = findInList(right.getFilter(), l, left.getFilter()); 342 if (r == null) { 343 union.getFilter().add(l); 344 res.updateContentState(true); 345 combined.getChildren().add(new StructuralMatch<Element>(l, vmI(IssueSeverity.ERROR, "Removed this item", "ValueSet.compose.include.filter"))); 346 session.markDeleted(right, "filter", l); 347 } else { 348 matchFR.add(r); 349 if (l.getProperty().equals(r.getProperty()) && l.getOp().equals(r.getOp())) { 350 ConceptSetFilterComponent cu = new ConceptSetFilterComponent(); 351 ConceptSetFilterComponent ci = new ConceptSetFilterComponent(); 352 union.getFilter().add(cu); 353 intersection.getFilter().add(ci); 354 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r); 355 combined.getChildren().add(sm); 356 if (compareFilters(l, r, sm, cu, ci)) { 357 res.updateContentState(true); 358 session.markChanged(r, l); 359 } 360 } else { 361 union.getFilter().add(l); 362 union.getFilter().add(r); 363 StructuralMatch<Element> sm = new StructuralMatch<Element>(l, r, vmI(IssueSeverity.WARNING, "Codes are different", "ValueSet.compose.include.filter")); 364 res.updateContentState(true); 365 combined.getChildren().add(sm); 366 compareFilters(l, r, sm, null, null); 367 } 368 } 369 } 370 for (ConceptSetFilterComponent r : right.getFilter()) { 371 if (!matchFR.contains(r)) { 372 union.getFilter().add(r); 373 res.updateContentState(true); 374 combined.getChildren().add(new StructuralMatch<Element>(vmI(IssueSeverity.ERROR, "Added this item", "ValueSet.compose.include.filter"), r)); 375 session.markAdded(r); 376 } 377 } 378 return def; 379 } 380 381 private boolean compareConcepts(String path, ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch<Element> sm, ConceptReferenceComponent cu, ConceptReferenceComponent ci, ValueSetComparison res) { 382 boolean def = false; 383 sm.getChildren().add(new StructuralMatch<Element>(l.getCodeElement(), r.getCodeElement(), l.getCode().equals(r.getCode()) ? null : vmI(IssueSeverity.INFORMATION, "Codes do not match", "ValueSet.compose.include.concept"))); 384 if (ci != null) { 385 ci.setCode(l.getCode()); 386 cu.setCode(l.getCode()); 387 } 388 if (l.hasDisplay() && r.hasDisplay()) { 389 sm.getChildren().add(new StructuralMatch<Element>(l.getDisplayElement(), r.getDisplayElement(), l.getDisplay().equals(r.getDisplay()) ? null : vmI(IssueSeverity.INFORMATION, "Displays do not match", "ValueSet.compose.include.concept"))); 390 if (ci != null) { 391 ci.setDisplay(r.getDisplay()); 392 cu.setDisplay(r.getDisplay()); 393 } 394 def = !l.getDisplay().equals(r.getDisplay()); 395 if (def) { 396 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display changed from '"+l.getDisplay()+"' to '"+r.getDisplay()+"'", IssueSeverity.WARNING)); 397 session.markChanged(r.getDisplayElement(), l.getDisplayElement()); 398 } 399 } else if (l.hasDisplay()) { 400 session.markDeleted(r, "display", l.getDisplayElement()); 401 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display '"+l.getDisplay()+"' removed", IssueSeverity.WARNING)); 402 sm.getChildren().add(new StructuralMatch<Element>(l.getDisplayElement(), null, vmI(IssueSeverity.INFORMATION, "Display Removed", "ValueSet.compose.include.concept"))); 403 if (ci != null) { 404 ci.setDisplay(l.getDisplay()); 405 cu.setDisplay(l.getDisplay()); 406 } 407 def = true; 408 } else if (r.hasDisplay()) { 409 session.markAdded(r.getDisplayElement()); 410 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display '"+r.getDisplay()+"' added", IssueSeverity.WARNING)); 411 sm.getChildren().add(new StructuralMatch<Element>(null, r.getDisplayElement(), vmI(IssueSeverity.INFORMATION, "Display added", "ValueSet.compose.include.concept"))); 412 if (ci != null) { 413 ci.setDisplay(r.getDisplay()); 414 cu.setDisplay(r.getDisplay()); 415 } 416 def = true; 417 } else { 418 sm.getChildren().add(new StructuralMatch<Element>(null, null, vmI(IssueSeverity.INFORMATION, "No Display", "ValueSet.compose.include.concept"))); 419 } 420 return def; 421 } 422 423 private boolean compareFilters(ConceptSetFilterComponent l, ConceptSetFilterComponent r, StructuralMatch<Element> sm, ConceptSetFilterComponent cu, ConceptSetFilterComponent ci) { 424 sm.getChildren().add(new StructuralMatch<Element>(l.getPropertyElement(), r.getPropertyElement(), l.getProperty().equals(r.getProperty()) ? null : vmI(IssueSeverity.INFORMATION, "Properties do not match", "ValueSet.compose.include.concept"))); 425 sm.getChildren().add(new StructuralMatch<Element>(l.getOpElement(), r.getOpElement(), l.getOp().equals(r.getOp()) ? null : vmI(IssueSeverity.INFORMATION, "Filter Operations do not match", "ValueSet.compose.include.concept"))); 426 sm.getChildren().add(new StructuralMatch<Element>(l.getValueElement(), r.getValueElement(), l.getValue().equals(r.getValue()) ? null : vmI(IssueSeverity.INFORMATION, "Values do not match", "ValueSet.compose.include.concept"))); 427 if (ci != null) { 428 ci.setProperty(l.getProperty()); 429 ci.setOp(l.getOp()); 430 ci.setValue(l.getValue()); 431 cu.setProperty(l.getProperty()); 432 cu.setOp(l.getOp()); 433 cu.setValue(l.getValue()); 434 } 435 return !l.getValue().equals(r.getValue()); 436 } 437 438 private CanonicalType findInList(List<CanonicalType> matches, CanonicalType item, List<CanonicalType> source) { 439 if (matches.size() == 1 && source.size() == 1) { 440 return matches.get(0); 441 } 442 for (CanonicalType t : matches) { 443 if (t.getValue().equals(item.getValue())) { 444 return t; 445 } 446 } 447 return null; 448 } 449 450 private ConceptReferenceComponent findInList(List<ConceptReferenceComponent> matches, ConceptReferenceComponent item, List<ConceptReferenceComponent> source) { 451 if (matches.size() == 1 && source.size() == 1) { 452 return matches.get(0); 453 } 454 for (ConceptReferenceComponent t : matches) { 455 if (t.getCode().equals(item.getCode())) { 456 return t; 457 } 458 } 459 return null; 460 } 461 462 private ConceptSetFilterComponent findInList(List<ConceptSetFilterComponent> matches, ConceptSetFilterComponent item, List<ConceptSetFilterComponent> source) { 463 if (matches.size() == 1 && source.size() == 1) { 464 return matches.get(0); 465 } 466 for (ConceptSetFilterComponent t : matches) { 467 if (t.getProperty().equals(item.getProperty()) && t.getOp().equals(item.getOp()) ) { 468 return t; 469 } 470 } 471 return null; 472 } 473 474 private void compareExpansions(ValueSet left, ValueSet right, ValueSetComparison res) { 475 ValueSet expL = left.hasExpansion() ? left : expand(left, res, "left", session.getContextLeft()); 476 ValueSet expR = right.hasExpansion() ? right : expand(right, res, "right", session.getContextRight()); 477 if (expL != null && expR != null) { 478 // ignore the parameters for now 479 compareConcepts(expL.getExpansion().getContains(), expR.getExpansion().getContains(), res.forceExpansion(), res.getUnion().getExpansion().getContains(), res.getIntersection().getExpansion().getContains(), "ValueSet.expansion.contains", res); 480 } 481 } 482 483 private ValueSet expand(ValueSet vs, ValueSetComparison res, String name, IWorkerContext ctxt) { 484 ValueSetExpansionOutcome vse = ctxt.expandVS(vs, true, false); 485 if (vse.getValueset() != null) { 486 return vse.getValueset(); 487 } else { 488 res.getMessages().add(new ValidationMessage(Source.TerminologyEngine, IssueType.EXCEPTION, "ValueSet", "Error Expanding "+name+":"+vse.getError(), IssueSeverity.ERROR)); 489 return null; 490 } 491 } 492 493 private void compareConcepts(List<ValueSetExpansionContainsComponent> left, List<ValueSetExpansionContainsComponent> right, StructuralMatch<ValueSetExpansionContainsComponent> combined, List<ValueSetExpansionContainsComponent> union, List<ValueSetExpansionContainsComponent> intersection, String path, ValueSetComparison res) { 494 List<ValueSetExpansionContainsComponent> matchR = new ArrayList<>(); 495 for (ValueSetExpansionContainsComponent l : left) { 496 ValueSetExpansionContainsComponent r = findInList(right, l); 497 if (r == null) { 498 union.add(l); 499 combined.getChildren().add(new StructuralMatch<ValueSetExpansionContainsComponent>(l, vmI(IssueSeverity.INFORMATION, "Removed from expansion", path))); 500 } else { 501 matchR.add(r); 502 ValueSetExpansionContainsComponent ccU = merge(l, r); 503 ValueSetExpansionContainsComponent ccI = intersect(l, r); 504 union.add(ccU); 505 intersection.add(ccI); 506 StructuralMatch<ValueSetExpansionContainsComponent> sm = new StructuralMatch<ValueSetExpansionContainsComponent>(l, r); 507 compareItem(sm.getMessages(), path, l, r, res); 508 combined.getChildren().add(sm); 509 compareConcepts(l.getContains(), r.getContains(), sm, ccU.getContains(), ccI.getContains(), path+".where(code = '"+l.getCode()+"').contains", res); 510 } 511 } 512 for (ValueSetExpansionContainsComponent r : right) { 513 if (!matchR.contains(r)) { 514 union.add(r); 515 combined.getChildren().add(new StructuralMatch<ValueSetExpansionContainsComponent>(vmI(IssueSeverity.INFORMATION, "Added to expansion", path), r)); 516 } 517 } 518 } 519 520 private void compareItem(List<ValidationMessage> msgs, String path, ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r, ValueSetComparison res) { 521 compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res); 522 } 523 524 private void compareStrings(String path, List<ValidationMessage> msgs, String left, String right, String name, IssueSeverity level, ValueSetComparison res) { 525 if (!Utilities.noString(right)) { 526 if (Utilities.noString(left)) { 527 msgs.add(vmI(level, "Value for "+name+" added", path)); 528 } else if (!left.equals(right)) { 529 if (level != IssueSeverity.NULL) { 530 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path+".name", "Changed value for "+name+": '"+left+"' vs '"+right+"'", level)); 531 } 532 msgs.add(vmI(level, name+" changed from left to right", path)); 533 } 534 } else if (!Utilities.noString(left)) { 535 msgs.add(vmI(level, "Value for "+name+" removed", path)); 536 } 537 } 538 539 private ValueSetExpansionContainsComponent findInList(List<ValueSetExpansionContainsComponent> list, ValueSetExpansionContainsComponent item) { 540 for (ValueSetExpansionContainsComponent t : list) { 541 if (t.getSystem().equals(item.getSystem()) && t.getCode().equals(item.getCode())) { 542 return t; 543 } 544 } 545 return null; 546 } 547 548 private ValueSetExpansionContainsComponent intersect(ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r) { 549 ValueSetExpansionContainsComponent res = new ValueSetExpansionContainsComponent(); 550 if (l.hasAbstract() && r.hasAbstract()) { 551 res.setAbstract(l.getAbstract()); 552 } 553 if (l.hasCode() && r.hasCode()) { 554 res.setCode(l.getCode()); 555 } 556 if (l.hasSystem() && r.hasSystem()) { 557 res.setSystem(l.getSystem()); 558 } 559 if (l.hasVersion() && r.hasVersion()) { 560 res.setVersion(l.getVersion()); 561 } 562 if (l.hasDisplay() && r.hasDisplay()) { 563 res.setDisplay(l.getDisplay()); 564 } 565 return res; 566 } 567 568 private ValueSetExpansionContainsComponent merge(ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r) { 569 ValueSetExpansionContainsComponent res = new ValueSetExpansionContainsComponent(); 570 if (l.hasAbstract()) { 571 res.setAbstract(l.getAbstract()); 572 } else if (r.hasAbstract()) { 573 res.setAbstract(r.getAbstract()); 574 } 575 if (l.hasCode()) { 576 res.setCode(l.getCode()); 577 } else if (r.hasCode()) { 578 res.setCode(r.getCode()); 579 } 580 if (l.hasSystem()) { 581 res.setSystem(l.getSystem()); 582 } else if (r.hasSystem()) { 583 res.setSystem(r.getSystem()); 584 } 585 if (l.hasVersion()) { 586 res.setVersion(l.getVersion()); 587 } else if (r.hasVersion()) { 588 res.setVersion(r.getVersion()); 589 } 590 if (l.hasDisplay()) { 591 res.setDisplay(l.getDisplay()); 592 } else if (r.hasDisplay()) { 593 res.setDisplay(r.getDisplay()); 594 } 595 return res; 596 } 597 598 @Override 599 protected String fhirType() { 600 return "ValueSet"; 601 } 602 603 public XhtmlNode renderCompose(ValueSetComparison csc, String id, String prefix) throws FHIRException, IOException { 604 HierarchicalTableGenerator gen = new HierarchicalTableGenerator(new RenderingI18nContext(), Utilities.path("[tmp]", "comparison"), false, "c"); 605 TableModel model = gen.new TableModel(id, true); 606 model.setAlternating(true); 607 model.getTitles().add(gen.new Title(null, null, "Item", "The type of item being compared", null, 100)); 608 model.getTitles().add(gen.new Title(null, null, "Property", "The system for the concept", null, 100, 2)); 609 model.getTitles().add(gen.new Title(null, null, "Value", "The display for the concept", null, 200, 2)); 610 model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); 611 for (StructuralMatch<Element> t : csc.getIncludes().getChildren()) { 612 addComposeRow(gen, model.getRows(), t, "include"); 613 } 614 for (StructuralMatch<Element> t : csc.getExcludes().getChildren()) { 615 addComposeRow(gen, model.getRows(), t, "exclude"); 616 } 617 return gen.generate(model, prefix, 0, null); 618 } 619 620 private void addComposeRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<Element> t, String name) { 621 Row r = gen.new Row(); 622 rows.add(r); 623 r.getCells().add(gen.new Cell(null, null, name, null, null)); 624 if (t.hasLeft() && t.hasRight()) { 625 ConceptSetComponent csL = (ConceptSetComponent) t.getLeft(); 626 ConceptSetComponent csR = (ConceptSetComponent) t.getRight(); 627 if (csL.hasSystem() && csL.getSystem().equals(csR.getSystem())) { 628 r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).span(2).center()); 629 } else { 630 r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 631 r.getCells().add(gen.new Cell(null, null, csR.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 632 } 633 634 if (csL.hasVersion() && csR.hasVersion()) { 635 if (csL.getVersion().equals(csR.getVersion())) { 636 r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).span(2).center()); 637 } else { 638 r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 639 r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 640 } 641 } else if (csL.hasVersion()) { 642 r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null)); 643 r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); 644 } else if (csR.hasVersion()) { 645 r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); 646 r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null)); 647 } else { 648 r.getCells().add(missingCell(gen).span(2).center()); 649 } 650 651 } else if (t.hasLeft()) { 652 r.setColor(COLOR_NO_ROW_RIGHT); 653 ConceptSetComponent cs = (ConceptSetComponent) t.getLeft(); 654 r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); 655 r.getCells().add(missingCell(gen)); 656 r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); 657 r.getCells().add(missingCell(gen)); 658 } else { 659 r.setColor(COLOR_NO_ROW_LEFT); 660 ConceptSetComponent cs = (ConceptSetComponent) t.getRight(); 661 r.getCells().add(missingCell(gen)); 662 r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); 663 r.getCells().add(missingCell(gen)); 664 r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); 665 } 666 r.getCells().add(cellForMessages(gen, t.getMessages())); 667 for (StructuralMatch<Element> c : t.getChildren()) { 668 if (c.either() instanceof ConceptReferenceComponent) { 669 addSetConceptRow(gen, r.getSubRows(), c); 670 } else { 671 addSetFilterRow(gen, r.getSubRows(), c); 672 } 673 } 674 } 675 676 private void addSetConceptRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<Element> t) { 677 Row r = gen.new Row(); 678 rows.add(r); 679 r.getCells().add(gen.new Cell(null, null, "Concept", null, null)); 680 if (t.hasLeft() && t.hasRight()) { 681 ConceptReferenceComponent csL = (ConceptReferenceComponent) t.getLeft(); 682 ConceptReferenceComponent csR = (ConceptReferenceComponent) t.getRight(); 683 // we assume both have codes 684 if (csL.getCode().equals(csR.getCode())) { 685 r.getCells().add(gen.new Cell(null, null, csL.getCode(), null, null).span(2).center()); 686 } else { 687 r.getCells().add(gen.new Cell(null, null, csL.getCode(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 688 r.getCells().add(gen.new Cell(null, null, csR.getCode(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 689 } 690 691 if (csL.hasDisplay() && csR.hasDisplay()) { 692 if (csL.getDisplay().equals(csR.getDisplay())) { 693 r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null).span(2).center()); 694 } else { 695 r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 696 r.getCells().add(gen.new Cell(null, null, csR.getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 697 } 698 } else if (csL.hasDisplay()) { 699 r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null)); 700 r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); 701 } else if (csR.hasDisplay()) { 702 r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); 703 r.getCells().add(gen.new Cell(null, null, csR.getDisplay(), null, null)); 704 } else { 705 r.getCells().add(missingCell(gen).span(2).center()); 706 } 707 708 } else if (t.hasLeft()) { 709 r.setColor(COLOR_NO_ROW_RIGHT); 710 ConceptReferenceComponent cs = (ConceptReferenceComponent) t.getLeft(); 711 r.getCells().add(gen.new Cell(null, null, cs.getCode(), null, null)); 712 r.getCells().add(missingCell(gen)); 713 r.getCells().add(gen.new Cell(null, null, cs.hasDisplay() ? "Version: "+cs.getDisplay() : "", null, null)); 714 r.getCells().add(missingCell(gen)); 715 } else { 716 r.setColor(COLOR_NO_ROW_LEFT); 717 ConceptReferenceComponent cs = (ConceptReferenceComponent) t.getRight(); 718 r.getCells().add(missingCell(gen)); 719 r.getCells().add(gen.new Cell(null, null, cs.getCode(), null, null)); 720 r.getCells().add(missingCell(gen)); 721 r.getCells().add(gen.new Cell(null, null, cs.hasDisplay() ? "Version: "+cs.getDisplay() : "", null, null)); 722 } 723 r.getCells().add(cellForMessages(gen, t.getMessages())); 724 725 } 726 727 private void addSetFilterRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<Element> t) { 728// Row r = gen.new Row(); 729// rows.add(r); 730// r.getCells().add(gen.new Cell(null, null, "Filter", null, null)); 731// if (t.hasLeft() && t.hasRight()) { 732// ConceptSetComponent csL = (ConceptSetComponent) t.getLeft(); 733// ConceptSetComponent csR = (ConceptSetComponent) t.getRight(); 734// // we assume both have systems 735// if (csL.getSystem().equals(csR.getSystem())) { 736// r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).span(2).center()); 737// } else { 738// r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 739// r.getCells().add(gen.new Cell(null, null, csR.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 740// } 741// 742// if (csL.hasVersion() && csR.hasVersion()) { 743// if (csL.getVersion().equals(csR.getVersion())) { 744// r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).span(2).center()); 745// } else { 746// r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 747// r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 748// } 749// } else if (csL.hasVersion()) { 750// r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null)); 751// r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); 752// } else if (csR.hasVersion()) { 753// r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); 754// r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null)); 755// } else { 756// r.getCells().add(missingCell(gen).span(2).center()); 757// } 758// 759// } else if (t.hasLeft()) { 760// r.setColor(COLOR_NO_ROW_RIGHT); 761// ConceptSetComponent cs = (ConceptSetComponent) t.getLeft(); 762// r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); 763// r.getCells().add(missingCell(gen)); 764// r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); 765// r.getCells().add(missingCell(gen)); 766// } else { 767// r.setColor(COLOR_NO_ROW_LEFT); 768// ConceptSetComponent cs = (ConceptSetComponent) t.getRight(); 769// r.getCells().add(missingCell(gen)); 770// r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); 771// r.getCells().add(missingCell(gen)); 772// r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); 773// } 774// r.getCells().add(gen.new Cell(null, null, t.getError(), null, null)); 775 776 } 777 778 public XhtmlNode renderExpansion(ValueSetComparison csc, String id, String prefix) throws IOException { 779 if (csc.getExpansion() == null) { 780 XhtmlNode p = new XhtmlNode(NodeType.Element, "p"); 781 p.tx("Unable to generate expansion - see errors"); 782 return p; 783 } 784 if (csc.getExpansion().getChildren().isEmpty()) { 785 XhtmlNode p = new XhtmlNode(NodeType.Element, "p"); 786 p.tx("Expansion is empty"); 787 return p; 788 } 789 // columns: code(+system), version, display , abstract, inactive, 790 boolean hasSystem = csc.getExpansion().getChildren().isEmpty() ? false : getSystemVaries(csc.getExpansion(), csc.getExpansion().getChildren().get(0).either().getSystem()); 791 boolean hasVersion = findVersion(csc.getExpansion()); 792 boolean hasAbstract = findAbstract(csc.getExpansion()); 793 boolean hasInactive = findInactive(csc.getExpansion()); 794 795 HierarchicalTableGenerator gen = new HierarchicalTableGenerator(new RenderingI18nContext(), Utilities.path("[tmp]", "comparison"), false, "c"); 796 TableModel model = gen.new TableModel(id, true); 797 model.setAlternating(true); 798 if (hasSystem) { 799 model.getTitles().add(gen.new Title(null, null, "System", "The code for the concept", null, 100)); 800 } 801 model.getTitles().add(gen.new Title(null, null, "Code", "The system for the concept", null, 100)); 802 model.getTitles().add(gen.new Title(null, null, "Display", "The display for the concept", null, 200, 2)); 803// if (hasVersion) { 804// model.getTitles().add(gen.new Title(null, null, "Version", "The version for the concept", null, 200, 2)); 805// } 806// if (hasAbstract) { 807// model.getTitles().add(gen.new Title(null, null, "Abstract", "The abstract flag for the concept", null, 200, 2)); 808// } 809// if (hasInactive) { 810// model.getTitles().add(gen.new Title(null, null, "Inactive", "The inactive flag for the concept", null, 200, 2)); 811// } 812 model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); 813 for (StructuralMatch<ValueSetExpansionContainsComponent> t : csc.getExpansion().getChildren()) { 814 addExpansionRow(gen, model.getRows(), t, hasSystem, hasVersion, hasAbstract, hasInactive); 815 } 816 return gen.generate(model, prefix, 0, null); 817 } 818 819 private void addExpansionRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<ValueSetExpansionContainsComponent> t, boolean hasSystem, boolean hasVersion, boolean hasAbstract, boolean hasInactive) { 820 Row r = gen.new Row(); 821 rows.add(r); 822 if (hasSystem) { 823 r.getCells().add(gen.new Cell(null, null, t.either().getSystem(), null, null)); 824 } 825 r.getCells().add(gen.new Cell(null, null, t.either().getCode(), null, null)); 826 if (t.hasLeft() && t.hasRight()) { 827 if (t.getLeft().hasDisplay() && t.getRight().hasDisplay()) { 828 if (t.getLeft().getDisplay().equals(t.getRight().getDisplay())) { 829 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2).center()); 830 } else { 831 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 832 r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 833 } 834 } else if (t.getLeft().hasDisplay()) { 835 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null)); 836 r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); 837 } else if (t.getRight().hasDisplay()) { 838 r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); 839 r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null)); 840 } else { 841 r.getCells().add(missingCell(gen).span(2).center()); 842 } 843 844 } else if (t.hasLeft()) { 845 r.setColor(COLOR_NO_ROW_RIGHT); 846 r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); 847 r.getCells().add(missingCell(gen)); 848 } else { 849 r.setColor(COLOR_NO_ROW_LEFT); 850 r.getCells().add(missingCell(gen)); 851 r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); 852 } 853 r.getCells().add(cellForMessages(gen, t.getMessages())); 854 for (StructuralMatch<ValueSetExpansionContainsComponent> c : t.getChildren()) { 855 addExpansionRow(gen, r.getSubRows(), c, hasSystem, hasVersion, hasAbstract, hasInactive); 856 } 857 } 858 859 private boolean getSystemVaries(StructuralMatch<ValueSetExpansionContainsComponent> list, String system) { 860 for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) { 861 if (t.hasLeft() && !system.equals(t.getLeft().getSystem())) { 862 return true; 863 } 864 if (t.hasRight() && !system.equals(t.getRight().getSystem())) { 865 return true; 866 } 867 if (getSystemVaries(t, system)) { 868 return true; 869 } 870 } 871 return false; 872 } 873 874 private boolean findInactive(StructuralMatch<ValueSetExpansionContainsComponent> list) { 875 for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) { 876 if (t.hasLeft() && t.getLeft().getInactive()) { 877 return true; 878 } 879 if (t.hasRight() && t.getRight().getInactive()) { 880 return true; 881 } 882 if (findInactive(t)) { 883 return true; 884 } 885 } 886 return false; 887 } 888 889 private boolean findAbstract(StructuralMatch<ValueSetExpansionContainsComponent> list) { 890 for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) { 891 if (t.hasLeft() && t.getLeft().getAbstract()) { 892 return true; 893 } 894 if (t.hasRight() && t.getRight().getAbstract()) { 895 return true; 896 } 897 if (findAbstract(t)) { 898 return true; 899 } 900 } 901 return false; 902 } 903 904 private boolean findVersion(StructuralMatch<ValueSetExpansionContainsComponent> list) { 905 for (StructuralMatch<ValueSetExpansionContainsComponent> t : list.getChildren()) { 906 if (t.hasLeft() && t.getLeft().hasVersion()) { 907 return true; 908 } 909 if (t.hasRight() && t.getRight().hasVersion()) { 910 return true; 911 } 912 if (findVersion(t)) { 913 return true; 914 } 915 } 916 return false; 917 } 918 919 920 public XhtmlNode renderUnion(ValueSetComparison comp, String id, String prefix, String corePath) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome { 921 ValueSetRenderer vsr = new ValueSetRenderer(new RenderingContext(session.getContextLeft(), null, new ValidationOptions(), corePath, prefix, null, ResourceRendererMode.TECHNICAL, GenerationRules.IG_PUBLISHER)); 922 return vsr.buildNarrative(ResourceWrapper.forResource(vsr.getContext(), comp.union)); 923 } 924 925 public XhtmlNode renderIntersection(ValueSetComparison comp, String id, String prefix, String corePath) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome { 926 ValueSetRenderer vsr = new ValueSetRenderer(new RenderingContext(session.getContextLeft(), null, new ValidationOptions(), corePath, prefix, null, ResourceRendererMode.TECHNICAL, GenerationRules.IG_PUBLISHER)); 927 return vsr.buildNarrative(ResourceWrapper.forResource(vsr.getContext(), comp.intersection)); 928 } 929 930}