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