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