001package org.hl7.fhir.r5.comparison; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Date; 006import java.util.HashMap; 007import java.util.List; 008import java.util.Map; 009 010import org.hl7.fhir.exceptions.DefinitionException; 011import org.hl7.fhir.exceptions.FHIRException; 012import org.hl7.fhir.r5.comparison.ResourceComparer.MessageCounts; 013import org.hl7.fhir.r5.model.CodeSystem; 014import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 015import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 016import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; 017import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; 018import org.hl7.fhir.utilities.Utilities; 019import org.hl7.fhir.utilities.validation.ValidationMessage; 020import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 021import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 022import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 023import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; 024import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; 025import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; 026import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; 027import org.hl7.fhir.utilities.xhtml.XhtmlNode; 028 029public class CodeSystemComparer extends CanonicalResourceComparer { 030 031 032 public class CodeSystemComparison extends CanonicalResourceComparison<CodeSystem> { 033 034 private StructuralMatch<ConceptDefinitionComponent> combined; 035 private Map<String, String> propMap = new HashMap<>(); // right to left; left retains it's name 036 037 public CodeSystemComparison(CodeSystem left, CodeSystem right) { 038 super(left, right); 039 combined = new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(); // base 040 } 041 042 public Map<String, String> getPropMap() { 043 return propMap; 044 } 045 046 public StructuralMatch<ConceptDefinitionComponent> getCombined() { 047 return combined; 048 } 049 050 @Override 051 protected String abbreviation() { 052 return "cs"; 053 } 054 055 @Override 056 protected String summary() { 057 return "CodeSystem: "+left.present()+" vs "+right.present(); 058 } 059 060 @Override 061 protected String fhirType() { 062 return "CodeSystem"; 063 } 064 065 @Override 066 protected void countMessages(MessageCounts cnts) { 067 super.countMessages(cnts); 068 combined.countMessages(cnts); 069 } 070 071 } 072 073 private CodeSystem right; 074 075 public CodeSystemComparer(ComparisonSession session) { 076 super(session); 077 } 078 079 public CodeSystemComparison compare(CodeSystem left, CodeSystem right) { 080 if (left == null) 081 throw new DefinitionException("No CodeSystem provided (left)"); 082 if (right == null) 083 throw new DefinitionException("No CodeSystem provided (right)"); 084 085 086 CodeSystemComparison res = new CodeSystemComparison(left, right); 087 session.identify(res); 088 CodeSystem cs = new CodeSystem(); 089 res.setUnion(cs); 090 session.identify(cs); 091 cs.setName("Union"+left.getName()+"And"+right.getName()); 092 cs.setTitle("Union of "+left.getTitle()+" And "+right.getTitle()); 093 cs.setStatus(left.getStatus()); 094 cs.setDate(new Date()); 095 for (PropertyComponent pL : left.getProperty()) { 096 cs.addProperty(pL.copy()); 097 } 098 for (PropertyComponent pR : left.getProperty()) { 099 PropertyComponent pL = findProperty(left, pR); 100 if (pL == null) { 101 String code = getUniqued(pR.getCode(), cs.getProperty()); 102 cs.addProperty(pR.copy().setCode(code)); 103 } else { 104 res.getPropMap().put(pR.getCode(), pL.getCode()); 105 } 106 } 107 108 CodeSystem cs1 = new CodeSystem(); 109 res.setIntersection(cs1); 110 session.identify(cs1); 111 cs1.setName("Intersection"+left.getName()+"And"+right.getName()); 112 cs1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle()); 113 cs1.setStatus(left.getStatus()); 114 cs1.setDate(new Date()); 115 cs1.getProperty().addAll(cs.getProperty()); 116 117 compareMetadata(left, right, res.getMetadata(), res); 118 comparePrimitives("caseSensitive", left.getCaseSensitiveElement(), right.getCaseSensitiveElement(), res.getMetadata(), IssueSeverity.ERROR, res); 119 comparePrimitives("hierarchyMeaning", left.getHierarchyMeaningElement(), right.getHierarchyMeaningElement(), res.getMetadata(), IssueSeverity.ERROR, res); 120 comparePrimitives("compositional", left.getCompositionalElement(), right.getCompositionalElement(), res.getMetadata(), IssueSeverity.WARNING, res); 121 comparePrimitives("versionNeeded", left.getVersionNeededElement(), right.getVersionNeededElement(), res.getMetadata(), IssueSeverity.INFORMATION, res); 122 comparePrimitives("content", left.getContentElement(), right.getContentElement(), res.getMetadata(), IssueSeverity.WARNING, res); 123 124 compareConcepts(left.getConcept(), right.getConcept(), res.getCombined(), res.getUnion().getConcept(), res.getIntersection().getConcept(), res.getUnion(), res.getIntersection(), res, "CodeSystem.concept"); 125 return res; 126 } 127 128 private String getUniqued(String code, List<PropertyComponent> list) { 129 int i = 0; 130 while (true) { 131 boolean ok = true; 132 String res = code+(i == 0 ? "" : i); 133 for (PropertyComponent t : list) { 134 if (res.equals(t.getCode())) { 135 ok = false; 136 } 137 } 138 if (ok) { 139 return res; 140 } 141 } 142 } 143 144 private PropertyComponent findProperty(CodeSystem left, PropertyComponent p) { 145 for (PropertyComponent t : left.getProperty()) { 146 if (p.hasUri() && t.hasUri() && p.getUri().equals(t.getUri())) { 147 return t; 148 } else if (!p.hasUri() && !t.hasUri() && p.getCode().equals(t.getCode())) { 149 return t; 150 } 151 } 152 return null; 153 } 154 155 156 private void compareConcepts(List<ConceptDefinitionComponent> left, List<ConceptDefinitionComponent> right, StructuralMatch<ConceptDefinitionComponent> combined, 157 List<ConceptDefinitionComponent> union, List<ConceptDefinitionComponent> intersection, CodeSystem csU, CodeSystem csI, CodeSystemComparison res, String path) { 158 List<ConceptDefinitionComponent> matchR = new ArrayList<>(); 159 for (ConceptDefinitionComponent l : left) { 160 ConceptDefinitionComponent r = findInList(right, l); 161 if (r == null) { 162 union.add(l); 163 combined.getChildren().add(new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(l, vmI(IssueSeverity.INFORMATION, "Removed this concept", path))); 164 } else { 165 matchR.add(r); 166 ConceptDefinitionComponent cdM = merge(l, r, csU.getProperty(), res); 167 ConceptDefinitionComponent cdI = intersect(l, r, res); 168 union.add(cdM); 169 intersection.add(cdI); 170 StructuralMatch<ConceptDefinitionComponent> sm = new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(l, r); 171 compare(sm.getMessages(), l, r, path+".where(code='"+l.getCode()+"')", res); 172 combined.getChildren().add(sm); 173 compareConcepts(l.getConcept(), r.getConcept(), sm, cdM.getConcept(), cdI.getConcept(), csU, csI, res, path+".where(code='"+l.getCode()+"').concept"); 174 } 175 } 176 for (ConceptDefinitionComponent r : right) { 177 if (!matchR.contains(r)) { 178 union.add(r); 179 combined.getChildren().add(new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(vmI(IssueSeverity.INFORMATION, "Added this concept", path), r)); 180 } 181 } 182 } 183 184 private ConceptDefinitionComponent findInList(List<ConceptDefinitionComponent> list, ConceptDefinitionComponent item) { 185 for (ConceptDefinitionComponent t : list) { 186 if (t.getCode().equals(item.getCode())) { 187 return t; 188 } 189 } 190 return null; 191 } 192 193 private void compare(List<ValidationMessage> msgs, ConceptDefinitionComponent l, ConceptDefinitionComponent r, String path, CodeSystemComparison res) { 194 compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res); 195 compareStrings(path, msgs, l.getDefinition(), r.getDefinition(), "definition", IssueSeverity.INFORMATION, res); 196 } 197 198 private void compareStrings(String path, List<ValidationMessage> msgs, String left, String right, String name, IssueSeverity level, CodeSystemComparison res) { 199 if (!Utilities.noString(right)) { 200 if (Utilities.noString(left)) { 201 msgs.add(vmI(level, "Value for "+name+" added", path)); 202 } else if (!left.equals(right)) { 203 if (level != IssueSeverity.NULL) { 204 res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path+"."+name, "Changed value for "+name+": '"+left+"' vs '"+right+"'", level)); 205 } 206 msgs.add(vmI(level, name+" changed from left to right", path)); 207 } 208 } else if (!Utilities.noString(left)) { 209 msgs.add(vmI(level, "Value for "+name+" removed", path)); 210 } 211 } 212 213 private ConceptDefinitionComponent merge(ConceptDefinitionComponent l, ConceptDefinitionComponent r, List<PropertyComponent> destProps, CodeSystemComparison res) { 214 ConceptDefinitionComponent cd = l.copy(); 215 if (!l.hasDisplay() && r.hasDisplay()) { 216 cd.setDisplay(r.getDisplay()); 217 } 218 if (!l.hasDefinition() && r.hasDefinition()) { 219 cd.setDefinition(r.getDefinition()); 220 } 221 mergeProps(cd, l, r, destProps, res); 222 mergeDesignations(cd, l, r); 223 return cd; 224 } 225 226 private ConceptDefinitionComponent intersect(ConceptDefinitionComponent l, ConceptDefinitionComponent r, CodeSystemComparison res) { 227 ConceptDefinitionComponent cd = l.copy(); 228 if (l.hasDisplay() && !r.hasDisplay()) { 229 cd.setDisplay(null); 230 } 231 if (l.hasDefinition() && !r.hasDefinition()) { 232 cd.setDefinition(null); 233 } 234 intersectProps(cd, l, r, res); 235 // mergeDesignations(cd, l, r); 236 return cd; 237 } 238 239 private void mergeDesignations(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r) { 240 for (ConceptDefinitionDesignationComponent td : l.getDesignation()) { 241 if (hasDesignation(td, r.getDesignation())) { 242 cd.getDesignation().add(td); 243 } 244 } 245 for (ConceptDefinitionDesignationComponent td : r.getDesignation()) { 246 if (hasDesignation(td, l.getDesignation())) { 247 cd.getDesignation().add(td); 248 } 249 } 250 } 251 252 private boolean hasDesignation(ConceptDefinitionDesignationComponent td, List<ConceptDefinitionDesignationComponent> designation) { 253 for (ConceptDefinitionDesignationComponent t : designation) { 254 if (designationsMatch(td, t)) { 255 return true; 256 } 257 } 258 return false; 259 } 260 261 private boolean designationsMatch(ConceptDefinitionDesignationComponent l, ConceptDefinitionDesignationComponent r) { 262 if (l.hasUse() != r.hasUse()) { 263 return false; 264 } 265 if (l.hasLanguage() != r.hasLanguage()) { 266 return false; 267 } 268 if (l.hasValue() != r.hasValue()) { 269 return false; 270 } 271 if (l.hasUse()) { 272 if (l.getUse().equalsDeep(r.getUse())) { 273 return false; 274 } 275 } 276 if (l.hasLanguage()) { 277 if (l.getLanguageElement().equalsDeep(r.getLanguageElement())) { 278 return false; 279 } 280 } 281 if (l.hasValue()) { 282 if (l.getValueElement().equalsDeep(r.getValueElement())) { 283 return false; 284 } 285 } 286 return true; 287 } 288 289 private void mergeProps(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r, List<PropertyComponent> destProps, CodeSystemComparison res) { 290 List<ConceptPropertyComponent> matchR = new ArrayList<>(); 291 for (ConceptPropertyComponent lp : l.getProperty()) { 292 ConceptPropertyComponent rp = findRightProp(r.getProperty(), lp, res); 293 if (rp == null) { 294 cd.getProperty().add(lp); 295 } else { 296 matchR.add(rp); 297 cd.getProperty().add(lp); 298 if (lp.getValue().equalsDeep(rp.getValue())) { 299 cd.getProperty().add(rp.setCode(res.getPropMap().get(rp.getCode()))); 300 } 301 } 302 } 303 for (ConceptPropertyComponent rp : r.getProperty()) { 304 if (!matchR.contains(rp)) { 305 cd.getProperty().add(rp.setCode(res.getPropMap().get(rp.getCode()))); 306 } 307 } 308 } 309 310 private void intersectProps(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r, CodeSystemComparison res) { 311 for (ConceptPropertyComponent lp : l.getProperty()) { 312 ConceptPropertyComponent rp = findRightProp(r.getProperty(), lp, res); 313 if (rp != null) { 314 cd.getProperty().add(lp); 315 } 316 } 317 } 318 319 private ConceptPropertyComponent findRightProp(List<ConceptPropertyComponent> rightProperties, ConceptPropertyComponent lp, CodeSystemComparison res) { 320 for (ConceptPropertyComponent p : rightProperties) { 321 if (res.getPropMap().get(p.getCode()).equals(lp.getCode())) { 322 return p; 323 } 324 } 325 return null; 326 } 327 328 public XhtmlNode renderConcepts(CodeSystemComparison comparison, String id, String prefix) throws FHIRException, IOException { 329 // columns: code, display (left|right), properties (left|right) 330 HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "compare"), false); 331 TableModel model = gen.new TableModel(id, true); 332 model.setAlternating(true); 333 model.getTitles().add(gen.new Title(null, null, "Code", "The code for the concept", null, 100)); 334 model.getTitles().add(gen.new Title(null, null, "Display", "The display for the concept", null, 200, 2)); 335 for (PropertyComponent p : comparison.getUnion().getProperty()) { 336 model.getTitles().add(gen.new Title(null, null, p.getCode(), p.getDescription(), null, 100, 2)); 337 } 338 model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); 339 for (StructuralMatch<ConceptDefinitionComponent> t : comparison.getCombined().getChildren()) { 340 addRow(gen, model.getRows(), t, comparison); 341 } 342 return gen.generate(model, prefix, 0, null); 343 } 344 345 private void addRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<ConceptDefinitionComponent> t, CodeSystemComparison comparison) { 346 Row r = gen.new Row(); 347 rows.add(r); 348 r.getCells().add(gen.new Cell(null, null, t.either().getCode(), null, null)); 349 if (t.hasLeft() && t.hasRight()) { 350 if (t.getLeft().hasDisplay() && t.getRight().hasDisplay()) { 351 if (t.getLeft().getDisplay().equals(t.getRight().getDisplay())) { 352 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2)); 353 } else { 354 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 355 r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); 356 } 357 } else if (t.getLeft().hasDisplay()) { 358 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null)); 359 r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); 360 } else if (t.getRight().hasDisplay()) { 361 r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); 362 r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null)); 363 } else { 364 r.getCells().add(missingCell(gen).span(2)); 365 } 366 for (PropertyComponent p : comparison.getUnion().getProperty()) { 367 ConceptPropertyComponent lp = getProp(t.getLeft(), p, false, comparison); 368 ConceptPropertyComponent rp = getProp(t.getRight(), p, true, comparison); 369 370 if (lp != null && rp != null) { 371 if (lp.getValue().equals(rp.getValue())) { 372 r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2)); 373 } else { 374 r.getCells().add(gen.new Cell(null, null, lp.getValue().toString(), null, null)); 375 r.getCells().add(gen.new Cell(null, null, rp.getValue().toString(), null, null)); 376 } 377 } else if (lp != null) { 378 r.getCells().add(gen.new Cell(null, null, lp.getValue().toString(), null, null)); 379 r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); 380 } else if (rp != null) { 381 r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); 382 r.getCells().add(gen.new Cell(null, null, rp.getValue().toString(), null, null)); 383 } else { 384 r.getCells().add(missingCell(gen).span(2)); 385 } 386 387 } 388 } else if (t.hasLeft()) { 389 r.setColor(COLOR_NO_ROW_RIGHT); 390 r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); 391 r.getCells().add(missingCell(gen)); 392 for (PropertyComponent p : comparison.getUnion().getProperty()) { 393 r.getCells().add(propertyCell(gen, t.getLeft(), p, false, comparison)); 394 r.getCells().add(missingCell(gen)); 395 } 396 } else { 397 r.setColor(COLOR_NO_ROW_LEFT); 398 r.getCells().add(missingCell(gen)); 399 r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); 400 for (PropertyComponent p : comparison.getUnion().getProperty()) { 401 r.getCells().add(missingCell(gen)); 402 r.getCells().add(propertyCell(gen, t.getLeft(), p, true, comparison)); 403 } 404 } 405 r.getCells().add(cellForMessages(gen, t.getMessages())); 406 } 407 408 private Cell propertyCell(HierarchicalTableGenerator gen, ConceptDefinitionComponent cd, PropertyComponent p, boolean right, CodeSystemComparison comp) { 409 ConceptPropertyComponent cp = getProp(cd, p, right, comp); 410 if (cp == null) { 411 return missingCell(gen, right ? COLOR_NO_CELL_RIGHT : COLOR_NO_CELL_LEFT); 412 } else { 413 return gen.new Cell(null, null, cp.getValue().toString(), null, null); 414 } 415 } 416 417 public ConceptPropertyComponent getProp(ConceptDefinitionComponent cd, PropertyComponent p, boolean right, CodeSystemComparison comp) { 418 String c = p.getCode(); 419 if (right) { 420 c = comp.getPropMap().get(c); 421 } 422 ConceptPropertyComponent cp = null; 423 if (cd != null) { 424 for (ConceptPropertyComponent t : cd.getProperty()) { 425 if (t.getCode().equals(c)) { 426 cp = t; 427 } 428 } 429 } 430 return cp; 431 } 432 433 @Override 434 protected String fhirType() { 435 return "CodeSystem"; 436 } 437 438}