001package org.hl7.fhir.convertors; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032 033import com.google.gson.JsonArray; 034import com.google.gson.JsonObject; 035import com.google.gson.JsonPrimitive; 036import org.hl7.fhir.convertors.conv40_50.resources40_50.StructureDefinition40_50; 037import org.hl7.fhir.convertors.conv40_50.resources40_50.ValueSet40_50; 038import org.hl7.fhir.exceptions.FHIRException; 039import org.hl7.fhir.exceptions.FHIRFormatError; 040import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; 041import org.hl7.fhir.r5.formats.IParser.OutputStyle; 042import org.hl7.fhir.r5.formats.JsonParser; 043import org.hl7.fhir.r5.formats.XmlParser; 044import org.hl7.fhir.r5.model.*; 045import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; 046import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent; 047import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 048import org.hl7.fhir.r5.model.Enumerations.BindingStrength; 049import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; 050import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 051import org.hl7.fhir.r5.utils.ToolingExtensions; 052import org.hl7.fhir.utilities.*; 053import org.hl7.fhir.utilities.xhtml.NodeType; 054import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 055import org.hl7.fhir.utilities.xhtml.XhtmlNode; 056import org.w3c.dom.Document; 057import org.w3c.dom.Element; 058import org.w3c.dom.Node; 059 060import java.io.ByteArrayOutputStream; 061import java.io.FileInputStream; 062import java.io.IOException; 063import java.util.*; 064 065public class SpecDifferenceEvaluator { 066 067 private final SpecPackage original = new SpecPackage(); 068 private final SpecPackage revision = new SpecPackage(); 069 private final Map<String, String> renames = new HashMap<String, String>(); 070 private final List<String> moves = new ArrayList<String>(); 071 private XhtmlNode tbl; 072 private TypeLinkProvider linker; 073 074 public static void main(String[] args) throws Exception { 075 System.out.println("gen diff"); 076 SpecDifferenceEvaluator self = new SpecDifferenceEvaluator(); 077 self.loadFromIni(new IniFile("C:\\work\\org.hl7.fhir\\build\\source\\fhir.ini")); 078// loadVS2(self.original.valuesets, "C:\\work\\org.hl7.fhir.dstu2.original\\build\\publish\\valuesets.xml"); 079// loadVS(self.revision.valuesets, "C:\\work\\org.hl7.fhir.dstu2.original\\build\\publish\\valuesets.xml"); 080 081 loadSD4(self.original.getTypes(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\profiles-types.xml"); 082 loadSD(self.revision.getTypes(), "C:\\work\\org.hl7.fhir\\build\\publish\\profiles-types.xml"); 083 loadSD4(self.original.getResources(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\profiles-resources.xml"); 084 loadSD(self.revision.getResources(), "C:\\work\\org.hl7.fhir\\build\\publish\\profiles-resources.xml"); 085 loadVS4(self.original.getExpansions(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\expansions.xml"); 086 loadVS(self.revision.getExpansions(), "C:\\work\\org.hl7.fhir\\build\\publish\\expansions.xml"); 087 loadVS4(self.original.getValuesets(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\valuesets.xml"); 088 loadVS(self.revision.getValuesets(), "C:\\work\\org.hl7.fhir\\build\\publish\\valuesets.xml"); 089 StringBuilder b = new StringBuilder(); 090 b.append("<html>\r\n"); 091 b.append("<head>\r\n"); 092 b.append("<link href=\"fhir.css\" rel=\"stylesheet\"/>\r\n"); 093 b.append("</head>\r\n"); 094 b.append("<body>\r\n"); 095 b.append(self.getDiffAsHtml(null)); 096 b.append("</body>\r\n"); 097 b.append("</html>\r\n"); 098 TextFile.stringToFile(b.toString(), "c:\\temp\\diff.html"); 099 System.out.println("done"); 100 } 101 102 private static void loadSD4(Map<String, StructureDefinition> map, String fn) throws FHIRException, IOException { 103 org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) new org.hl7.fhir.r4.formats.XmlParser().parse(new FileInputStream(fn)); 104 for (org.hl7.fhir.r4.model.Bundle.BundleEntryComponent be : bundle.getEntry()) { 105 if (be.getResource() instanceof org.hl7.fhir.r4.model.StructureDefinition) { 106 org.hl7.fhir.r4.model.StructureDefinition sd = (org.hl7.fhir.r4.model.StructureDefinition) be.getResource(); 107 map.put(sd.getName(), StructureDefinition40_50.convertStructureDefinition(sd)); 108 } 109 } 110 111 } 112 113 private static void loadSD(Map<String, StructureDefinition> map, String fn) throws FHIRFormatError, IOException { 114 Bundle bundle = (Bundle) new XmlParser().parse(new FileInputStream(fn)); 115 for (BundleEntryComponent be : bundle.getEntry()) { 116 if (be.getResource() instanceof StructureDefinition) { 117 StructureDefinition sd = (StructureDefinition) be.getResource(); 118 map.put(sd.getName(), sd); 119 } 120 } 121 } 122 123 private static void loadVS4(Map<String, ValueSet> map, String fn) throws FHIRException, IOException { 124 org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) new org.hl7.fhir.r4.formats.XmlParser().parse(new FileInputStream(fn)); 125 for (org.hl7.fhir.r4.model.Bundle.BundleEntryComponent be : bundle.getEntry()) { 126 if (be.getResource() instanceof org.hl7.fhir.r4.model.ValueSet) { 127 org.hl7.fhir.r4.model.ValueSet sd = (org.hl7.fhir.r4.model.ValueSet) be.getResource(); 128 map.put(sd.getName(), ValueSet40_50.convertValueSet(sd)); 129 } 130 } 131 } 132 133 private static void loadVS(Map<String, ValueSet> map, String fn) throws FHIRFormatError, IOException { 134 Bundle bundle = (Bundle) new XmlParser().parse(new FileInputStream(fn)); 135 for (BundleEntryComponent be : bundle.getEntry()) { 136 if (be.getResource() instanceof ValueSet) { 137 ValueSet sd = (ValueSet) be.getResource(); 138 map.put(sd.getName(), sd); 139 } 140 } 141 } 142 143 public void loadFromIni(IniFile ini) { 144 String[] names = ini.getPropertyNames("r5-renames"); 145 if (names != null) 146 for (String n : names) 147 // note reverse of order 148 renames.put(ini.getStringProperty("r5-renames", n), n); 149 } 150 151 public SpecPackage getOriginal() { 152 return original; 153 } 154 155 public SpecPackage getRevision() { 156 return revision; 157 } 158 159 public void getDiffAsJson(JsonObject json, StructureDefinition rev) throws IOException { 160 this.linker = null; 161 StructureDefinition orig = original.getResources().get(checkRename(rev.getName())); 162 if (orig == null) 163 orig = original.getTypes().get(checkRename(rev.getName())); 164 JsonArray types = new JsonArray(); 165 json.add("types", types); 166 types.add(new JsonPrimitive(rev.getName())); 167 JsonObject type = new JsonObject(); 168 json.add(rev.getName(), type); 169 if (orig == null) 170 type.addProperty("status", "new"); 171 else { 172 start(); 173 compareJson(type, orig, rev); 174 } 175 } 176 177 public void getDiffAsXml(Document doc, Element xml, StructureDefinition rev) throws IOException { 178 this.linker = null; 179 StructureDefinition orig = original.getResources().get(checkRename(rev.getName())); 180 if (orig == null) 181 orig = original.getTypes().get(checkRename(rev.getName())); 182 Element type = doc.createElement("type"); 183 type.setAttribute("name", rev.getName()); 184 xml.appendChild(type); 185 if (orig == null) 186 type.setAttribute("status", "new"); 187 else { 188 start(); 189 compareXml(doc, type, orig, rev); 190 } 191 } 192 193 public void getDiffAsJson(JsonObject json) throws IOException { 194 this.linker = null; 195 JsonArray types = new JsonArray(); 196 json.add("types", types); 197 198 for (String s : sorted(revision.getTypes().keySet())) { 199 StructureDefinition orig = original.getTypes().get(s); 200 StructureDefinition rev = revision.getTypes().get(s); 201 types.add(new JsonPrimitive(rev.getName())); 202 JsonObject type = new JsonObject(); 203 json.add(rev.getName(), type); 204 if (orig == null) { 205 type.addProperty("status", "new"); 206 } else if (rev.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { 207 type.addProperty("status", "no-change"); 208 } else if (rev.hasDerivation() && orig.hasDerivation() && rev.getDerivation() != orig.getDerivation()) { 209 type.addProperty("status", "status-change"); 210 type.addProperty("past-status", orig.getDerivation().toCode()); 211 type.addProperty("current-status", rev.getDerivation().toCode()); 212 } else { 213 compareJson(type, orig, rev); 214 } 215 } 216 for (String s : sorted(original.getTypes().keySet())) { 217 StructureDefinition orig = original.getTypes().get(s); 218 StructureDefinition rev = revision.getTypes().get(s); 219 if (rev == null) { 220 types.add(new JsonPrimitive(orig.getName())); 221 JsonObject type = new JsonObject(); 222 json.add(orig.getName(), type); 223 type.addProperty("status", "deleted"); 224 } 225 } 226 227 for (String s : sorted(revision.getResources().keySet())) { 228 StructureDefinition orig = original.getResources().get(checkRename(s)); 229 StructureDefinition rev = revision.getResources().get(s); 230 types.add(new JsonPrimitive(rev.getName())); 231 JsonObject type = new JsonObject(); 232 json.add(rev.getName(), type); 233 if (orig == null) { 234 type.addProperty("status", "new"); 235 } else { 236 compareJson(type, orig, rev); 237 } 238 } 239 for (String s : sorted(original.getResources().keySet())) { 240 StructureDefinition orig = original.getResources().get(s); 241 StructureDefinition rev = revision.getResources().get(s); 242 if (rev == null) { 243 types.add(new JsonPrimitive(orig.getName())); 244 JsonObject type = new JsonObject(); 245 json.add(orig.getName(), type); 246 type.addProperty("status", "deleted"); 247 } 248 } 249 } 250 251 public void getDiffAsXml(Document doc, Element xml) throws IOException { 252 this.linker = null; 253 254 for (String s : sorted(revision.getTypes().keySet())) { 255 StructureDefinition orig = original.getTypes().get(s); 256 StructureDefinition rev = revision.getTypes().get(s); 257 Element type = doc.createElement("type"); 258 type.setAttribute("name", rev.getName()); 259 xml.appendChild(type); 260 if (orig == null) { 261 type.setAttribute("status", "new"); 262 } else if (rev.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { 263 type.setAttribute("status", "no-change"); 264 } else if (rev.hasDerivation() && orig.hasDerivation() && rev.getDerivation() != orig.getDerivation()) { 265 type.setAttribute("status", "status-change"); 266 type.setAttribute("past-status", orig.getDerivation().toCode()); 267 type.setAttribute("current-status", rev.getDerivation().toCode()); 268 } else { 269 compareXml(doc, type, orig, rev); 270 } 271 } 272 for (String s : sorted(original.getTypes().keySet())) { 273 StructureDefinition orig = original.getTypes().get(s); 274 StructureDefinition rev = revision.getTypes().get(s); 275 if (rev == null) { 276 Element type = doc.createElement("type"); 277 type.setAttribute("name", orig.getName()); 278 xml.appendChild(type); 279 type.setAttribute("status", "deleted"); 280 } 281 } 282 283 for (String s : sorted(revision.getResources().keySet())) { 284 StructureDefinition orig = original.getResources().get(checkRename(s)); 285 StructureDefinition rev = revision.getResources().get(s); 286 Element type = doc.createElement("type"); 287 type.setAttribute("name", rev.getName()); 288 xml.appendChild(type); 289 if (orig == null) { 290 type.setAttribute("status", "new"); 291 } else { 292 compareXml(doc, type, orig, rev); 293 } 294 } 295 for (String s : sorted(original.getResources().keySet())) { 296 StructureDefinition orig = original.getResources().get(s); 297 StructureDefinition rev = revision.getResources().get(s); 298 if (rev == null) { 299 Element type = doc.createElement("type"); 300 type.setAttribute("name", orig.getName()); 301 xml.appendChild(type); 302 type.setAttribute("status", "deleted"); 303 } 304 } 305 } 306 307 public String getDiffAsHtml(TypeLinkProvider linker, StructureDefinition rev) throws IOException { 308 this.linker = linker; 309 310 StructureDefinition orig = original.getResources().get(checkRename(rev.getName())); 311 if (orig == null) 312 orig = original.getTypes().get(checkRename(rev.getName())); 313 if (orig == null) 314 return "<p>This " + rev.getKind().toCode() + " did not exist in Release 2</p>"; 315 else { 316 start(); 317 compare(orig, rev); 318 return new XhtmlComposer(false, true).compose(tbl) + "\r\n<p>See the <a href=\"diff.html\">Full Difference</a> for further information</p>\r\n"; 319 } 320 } 321 322 public String getDiffAsHtml(TypeLinkProvider linker) throws IOException { 323 this.linker = linker; 324 start(); 325 326 header("Types"); 327 for (String s : sorted(revision.getTypes().keySet())) { 328 StructureDefinition orig = original.getTypes().get(s); 329 StructureDefinition rev = revision.getTypes().get(s); 330 if (orig == null) { 331 markNew(rev.getName(), true, false, false); 332 } else if (rev.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { 333 markNoChanges(rev.getName(), true); 334 } else if (rev.hasDerivation() && orig.hasDerivation() && rev.getDerivation() != orig.getDerivation()) { 335 markChanged(rev.getName(), "Changed from a " + orig.getDerivation().toCode() + " to a " + rev.getDerivation().toCode(), true); 336 } else { 337 compare(orig, rev); 338 } 339 } 340 for (String s : sorted(original.getTypes().keySet())) { 341 StructureDefinition orig = original.getTypes().get(s); 342 StructureDefinition rev = revision.getTypes().get(s); 343 if (rev == null) 344 markDeleted(orig.getName(), true); 345 } 346 347 header("Resources"); 348 for (String s : sorted(revision.getResources().keySet())) { 349 StructureDefinition orig = original.getResources().get(checkRename(s)); 350 StructureDefinition rev = revision.getResources().get(s); 351 if (orig == null) { 352 markNew(rev.getName(), true, true, false); 353 } else { 354 compare(orig, rev); 355 } 356 } 357 for (String s : sorted(original.getResources().keySet())) { 358 StructureDefinition orig = original.getResources().get(s); 359 StructureDefinition rev = revision.getResources().get(s); 360 if (rev == null) 361 markDeleted(orig.getName(), true); 362 } 363 364 return new XhtmlComposer(false, true).compose(tbl); 365 } 366 367 private Object checkRename(String s) { 368 if (renames.containsKey(s)) 369 return renames.get(s); 370 else 371 return s; 372 } 373 374 private List<String> sorted(Set<String> keys) { 375 List<String> list = new ArrayList<String>(); 376 list.addAll(keys); 377 Collections.sort(list); 378 return list; 379 } 380 381 private void header(String title) { 382 tbl.addTag("tr").setAttribute("class", "diff-title").addTag("td").setAttribute("colspan", "2").addText(title); 383 } 384 385 private void start() { 386 tbl = new XhtmlNode(NodeType.Element, "table"); 387 tbl.setAttribute("class", "grid"); 388 389 } 390 391 private void markNoChanges(String name, boolean item) { 392 XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-item" : "diff-entry"); 393 XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left"); 394 XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right"); 395 String link = linker == null ? null : linker.getLink(name); 396 if (link != null) 397 left.addTag("a").setAttribute("href", link).addText(name); 398 else 399 left.addText(name); 400 right.span("opacity: 0.5", null).addText("(No Changes)"); 401 } 402 403 private void markChanged(String name, String change, boolean item) { 404 XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-item" : "diff-entry"); 405 XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left"); 406 XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right"); 407 String link = linker == null ? null : linker.getLink(name); 408 if (link != null) 409 left.addTag("a").setAttribute("href", link).addText(name); 410 else 411 left.addText(name); 412 right.ul().li().addText(change); 413 } 414 415 private void markDeleted(String name, boolean item) { 416 XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-del-item" : "diff-del"); 417 XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left"); 418 XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right"); 419 left.addText(name); 420 right.ul().li().addText("deleted"); 421 } 422 423 private void markNew(String name, boolean item, boolean res, boolean mand) { 424 XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-new-item" : "diff-new"); 425 XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left"); 426 XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right"); 427 String link = linker == null ? null : linker.getLink(name); 428 if (link != null) 429 left.addTag("a").setAttribute("href", link).addText(name); 430 else 431 left.addText(name); 432 if (!res && mand) 433 right.ul().li().b().addText("Added Mandatory Element"); 434 else 435 right.ul().li().addText(res ? "Added Resource" : !name.contains(".") ? "Added Type" : mand ? "Added Mandatory Element " : "Added Element"); 436 } 437 438 private void compare(StructureDefinition orig, StructureDefinition rev) { 439 moves.clear(); 440 XhtmlNode tr = tbl.addTag("tr").setAttribute("class", "diff-item"); 441 XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left"); 442 String link = linker == null ? null : linker.getLink(rev.getName()); 443 if (link != null) 444 left.addTag("a").setAttribute("href", link).addText(rev.getName()); 445 else 446 left.addText(rev.getName()); 447 XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right"); 448 449 // first, we must match revision elements to old elements 450 boolean changed = false; 451 if (!orig.getName().equals(rev.getName())) { 452 changed = true; 453 right.ul().li().addText("Name Changed from " + orig.getName() + " to " + rev.getName()); 454 } 455 for (ElementDefinition ed : rev.getDifferential().getElement()) { 456 ElementDefinition oed = getMatchingElement(rev.getName(), orig.getDifferential().getElement(), ed); 457 if (oed != null) { 458 ed.setUserData("match", oed); 459 oed.setUserData("match", ed); 460 } 461 } 462 463 for (ElementDefinition ed : rev.getDifferential().getElement()) { 464 ElementDefinition oed = (ElementDefinition) ed.getUserData("match"); 465 if (oed == null) { 466 changed = true; 467 markNew(ed.getPath(), false, false, ed.getMin() > 0); 468 } else 469 changed = compareElement(ed, oed) || changed; 470 } 471 472 List<String> dels = new ArrayList<String>(); 473 474 for (ElementDefinition ed : orig.getDifferential().getElement()) { 475 if (ed.getUserData("match") == null) { 476 changed = true; 477 boolean marked = false; 478 for (String s : dels) 479 if (ed.getPath().startsWith(s + ".")) 480 marked = true; 481 if (!marked) { 482 dels.add(ed.getPath()); 483 markDeleted(ed.getPath(), false); 484 } 485 } 486 } 487 488 if (!changed) 489 right.ul().li().addText("No Changes"); 490 491 for (ElementDefinition ed : rev.getDifferential().getElement()) 492 ed.clearUserData("match"); 493 for (ElementDefinition ed : orig.getDifferential().getElement()) 494 ed.clearUserData("match"); 495 496 } 497 498 private ElementDefinition getMatchingElement(String tn, List<ElementDefinition> list, ElementDefinition target) { 499 // now, look for matches by name (ignoring slicing for now) 500 String tp = mapPath(tn, target.getPath()); 501 if (tp.endsWith("[x]")) 502 tp = tp.substring(0, tp.length() - 4); 503 for (ElementDefinition ed : list) { 504 String p = ed.getPath(); 505 if (p.endsWith("[x]")) 506 p = p.substring(0, p.length() - 4); 507 if (p.equals(tp)) 508 return ed; 509 } 510 return null; 511 } 512 513 /** 514 * change from rev to original. TODO: make this a config file somewhere? 515 * 516 * @param tn 517 * @return 518 */ 519 private String mapPath(String tn, String path) { 520 if (renames.containsKey(path)) 521 return renames.get(path); 522 for (String r : renames.keySet()) { 523 if (path.startsWith(r + ".")) 524 return renames.get(r) + "." + path.substring(r.length() + 1); 525 } 526 return path; 527 } 528 529 private boolean compareElement(ElementDefinition rev, ElementDefinition orig) { 530 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("\r\n"); 531 String rn = tail(rev.getPath()); 532 String on = tail(orig.getPath()); 533 String rp = head(rev.getPath()); 534 String op = head(orig.getPath()); 535 boolean renamed = false; 536 if (!rn.equals(on) && rev.getPath().contains(".")) { 537 if (rp.equals(op)) 538 b.append("Renamed from " + on + " to " + rn); 539 else 540 b.append("Moved from " + orig.getPath() + " to " + rn); 541 renamed = true; 542 } else if (!rev.getPath().equals(orig.getPath())) { 543 if (!moveAlreadyNoted(rev.getPath(), orig.getPath())) { 544 noteMove(rev.getPath(), orig.getPath()); 545 b.append("Moved from " + head(orig.getPath()) + " to " + head(rev.getPath())); 546 renamed = true; 547 } 548 } 549 550 if (rev.getMin() != orig.getMin()) 551 b.append("Min Cardinality changed from " + orig.getMin() + " to " + rev.getMin()); 552 553 if (!rev.getMax().equals(orig.getMax())) 554 b.append("Max Cardinality changed from " + orig.getMax() + " to " + rev.getMax()); 555 556 analyseTypes(b, rev, orig); 557 558 if (hasBindingToNote(rev) || hasBindingToNote(orig)) { 559 String s = compareBindings(rev, orig); 560 if (!Utilities.noString(s)) 561 b.append(s); 562 } 563 564 if (rev.hasDefaultValue() || orig.hasDefaultValue()) { 565 if (!rev.hasDefaultValue()) 566 b.append("Default Value " + describeValue(orig.getDefaultValue()) + " removed"); 567 else if (!orig.hasDefaultValue()) 568 b.append("Default Value " + describeValue(rev.getDefaultValue()) + " added"); 569 else { 570 // do not use Base.compare here, because it is subject to type differences 571 String s1 = describeValue(orig.getDefaultValue()); 572 String s2 = describeValue(rev.getDefaultValue()); 573 if (!s1.equals(s2)) 574 b.append("Default Value changed from " + s1 + " to " + s2); 575 } 576 } 577 578 if (rev.getIsModifier() != orig.getIsModifier()) { 579 if (rev.getIsModifier()) 580 b.append("Now marked as Modifier"); 581 else 582 b.append("No longer marked as Modifier"); 583 } 584 585 if (b.length() > 0) { 586 XhtmlNode tr = tbl.addTag("tr").setAttribute("class", renamed ? "diff-changed-item" : "diff-entry"); 587 XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left"); 588 left.addText(rev.getPath()); 589 XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right"); 590 XhtmlNode ul = null; 591 for (String s : b.toString().split("\\r?\\n")) { 592 if (!Utilities.noString(s)) { 593 if (ul == null) 594 ul = right.addTag("ul"); 595 XhtmlNode li = ul.addTag("li").notPretty(); 596 if (s.contains("`")) { 597 String[] p = s.split("\\`"); 598 boolean code = true; 599 li.addText(p[0]); 600 for (int i = 1; i < p.length; i++) { 601 if (code) 602 li.addTag("code").addText(p[i]); 603 else 604 li.addText(p[i]); 605 code = !code; 606 } 607 } else 608 li.addText(s); 609 } 610 } 611 } 612 return b.length() > 0; 613 } 614 615 private void noteMove(String revpath, String origpath) { 616 moves.add(revpath + "=" + origpath); 617 } 618 619 private boolean moveAlreadyNoted(String revpath, String origpath) { 620 if (moves.contains(revpath + "=" + origpath)) 621 return true; 622 if (!revpath.contains(".") || !origpath.contains(".")) 623 return false; 624 return moveAlreadyNoted(head(revpath), head(origpath)); 625 } 626 627 @SuppressWarnings("rawtypes") 628 private String describeValue(DataType v) { 629 if (v instanceof PrimitiveType) { 630 return "\"" + ((PrimitiveType) v).asStringValue() + "\""; 631 } 632 return "{complex}"; 633 } 634 635 private String compareBindings(ElementDefinition rev, ElementDefinition orig) { 636 if (!hasBindingToNote(rev)) { 637 return "Remove Binding " + describeBinding(orig); 638 } else if (!hasBindingToNote(orig)) { 639 return "Add Binding " + describeBinding(rev); 640 } else { 641 return compareBindings(rev.getBinding(), orig.getBinding()); 642 } 643 } 644 645 private String compareBindings(ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) { 646 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("\r\n"); 647 if (rev.getStrength() != orig.getStrength()) 648 b.append("Change binding strength from " + orig.getStrength().toCode() + " to " + rev.getStrength().toCode()); 649 if (!Base.compareDeep(rev.getValueSet(), orig.getValueSet(), false)) 650 b.append("Change value set from " + describeReference(orig.getValueSet()) + " to " + describeReference(rev.getValueSet())); 651 if (!maxValueSetsMatch(rev, orig)) 652 b.append("Change max value set from " + describeMax(orig) + " to " + describeMax(rev)); 653 if (rev.getStrength() == BindingStrength.REQUIRED && orig.getStrength() == BindingStrength.REQUIRED) { 654 ValueSet vrev = getValueSet(rev.getValueSet(), revision.getExpansions()); 655 ValueSet vorig = getValueSet(rev.getValueSet(), original.getExpansions()); 656 CommaSeparatedStringBuilder br = new CommaSeparatedStringBuilder(); 657 int ir = 0; 658 CommaSeparatedStringBuilder bo = new CommaSeparatedStringBuilder(); 659 int io = 0; 660 if (vrev != null && vorig != null) { 661 for (ValueSetExpansionContainsComponent cc : vorig.getExpansion().getContains()) { 662 if (!hasCode(vrev, cc)) { 663 io++; 664 bo.append("`" + Utilities.escapeXml(cc.getCode()) + "`"); 665 } 666 } 667 for (ValueSetExpansionContainsComponent cc : vrev.getExpansion().getContains()) { 668 if (!hasCode(vorig, cc)) { 669 ir++; 670 br.append("`" + Utilities.escapeXml(cc.getCode()) + "`"); 671 } 672 } 673 } 674 if (io > 0) 675 b.append("Remove " + Utilities.pluralize("Code", io) + " " + bo); 676 if (ir > 0) 677 b.append("Add " + Utilities.pluralize("Code", ir) + " " + br); 678 679 } 680 if (rev.getStrength() == BindingStrength.EXTENSIBLE && orig.getStrength() == BindingStrength.EXTENSIBLE) { 681 ValueSet vrev = getValueSet(rev.getValueSet(), revision.getValuesets()); 682 ValueSet vorig = getValueSet(orig.getValueSet(), original.getValuesets()); 683 if (vrev != null && vrev.hasCompose() && vrev.getCompose().getInclude().size() == 1 && vrev.getCompose().getIncludeFirstRep().hasSystem() && 684 vorig != null && vorig.hasCompose() && vorig.getCompose().getInclude().size() == 1 && vorig.getCompose().getIncludeFirstRep().hasSystem()) { 685 if (!vorig.getCompose().getIncludeFirstRep().getSystem().equals(vrev.getCompose().getIncludeFirstRep().getSystem())) { 686 b.append("Change code system for extensibly bound codes from \"" + vorig.getCompose().getIncludeFirstRep().getSystem() + "\" to \"" + vrev.getCompose().getIncludeFirstRep().getSystem() + "\""); 687 } 688 } 689 } 690 691 return b.toString(); 692 } 693 694 private String describeMax(ElementDefinitionBindingComponent orig) { 695 if (!orig.hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) 696 return "n/a"; 697 return "`" + ToolingExtensions.readStringExtension(orig, ToolingExtensions.EXT_MAX_VALUESET) + "`"; 698 } 699 700 private boolean maxValueSetsMatch(ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) { 701 if (!rev.hasExtension(ToolingExtensions.EXT_MAX_VALUESET) && !orig.hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) 702 return true; 703 if (rev.hasExtension(ToolingExtensions.EXT_MAX_VALUESET) != orig.hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) 704 return false; 705 return ToolingExtensions.readStringExtension(rev, ToolingExtensions.EXT_MAX_VALUESET).equals(ToolingExtensions.readStringExtension(orig, ToolingExtensions.EXT_MAX_VALUESET)); 706 } 707 708// "Remove code "+ 709// "add code "+ 710 711 private String describeBinding(ElementDefinition orig) { 712 if (orig.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) 713 return "`" + orig.getBinding().getValueSet() + "` (" + orig.getBinding().getStrength().toCode() + "), max =`" + ToolingExtensions.readStringExtension(orig.getBinding(), ToolingExtensions.EXT_MAX_VALUESET) + "`"; 714 else 715 return "`" + orig.getBinding().getValueSet() + "` (" + orig.getBinding().getStrength().toCode() + ")"; 716 } 717 718 private void describeBinding(JsonObject element, String name, ElementDefinition orig) { 719 JsonObject binding = new JsonObject(); 720 element.add(name, binding); 721 binding.addProperty("reference", describeReference(orig.getBinding().getValueSet())); 722 binding.addProperty("strength", orig.getBinding().getStrength().toCode()); 723 if (orig.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) 724 binding.addProperty("max", ToolingExtensions.readStringExtension(orig.getBinding(), ToolingExtensions.EXT_MAX_VALUESET)); 725 } 726 727 private void describeBinding(Document doc, Element element, String name, ElementDefinition orig) { 728 Element binding = doc.createElement(name); 729 element.appendChild(binding); 730 binding.setAttribute("reference", describeReference(orig.getBinding().getValueSet())); 731 binding.setAttribute("strength", orig.getBinding().getStrength().toCode()); 732 if (orig.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) 733 binding.setAttribute("max", ToolingExtensions.readStringExtension(orig.getBinding(), ToolingExtensions.EXT_MAX_VALUESET)); 734 } 735 736 private String describeReference(String ref) { 737 return ref; 738 } 739 740 private ValueSet getValueSet(String ref, Map<String, ValueSet> expansions) { 741 if (ref != null) { 742 if (Utilities.isAbsoluteUrl(ref)) { 743 for (ValueSet ve : expansions.values()) { 744 if (ref.equals(ve.getUrl())) 745 return ve; 746 } 747 } else if (ref.startsWith("ValueSet/")) { 748 ref = ref.substring(9); 749 for (ValueSet ve : expansions.values()) { 750 if (ve.getId().equals(ref)) 751 return ve; 752 } 753 } 754 } 755 return null; 756 } 757 758 private String listCodes(ValueSet vs) { 759 if (vs.getExpansion().getContains().size() > 15) 760 return ">15 codes"; 761 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" | "); 762 for (ValueSetExpansionContainsComponent ce : vs.getExpansion().getContains()) { 763 if (ce.hasCode()) 764 b.append(ce.getCode()); 765 } 766 return b.toString(); 767 } 768 769 private boolean hasBindingToNote(ElementDefinition ed) { 770 return ed.hasBinding() && 771 (ed.getBinding().getStrength() == BindingStrength.EXTENSIBLE || ed.getBinding().getStrength() == BindingStrength.REQUIRED || ed.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) && 772 ed.getBinding().hasValueSet(); 773 } 774 775 private String tail(String path) { 776 return path.contains(".") ? path.substring(path.lastIndexOf(".") + 1) : path; 777 } 778 779 private String head(String path) { 780 return path.contains(".") ? path.substring(0, path.lastIndexOf(".")) : path; 781 } 782 783 private void analyseTypes(CommaSeparatedStringBuilder bp, ElementDefinition rev, ElementDefinition orig) { 784 if (rev.getType().size() == 1 && orig.getType().size() == 1) { 785 String r = describeType(rev.getType().get(0)); 786 if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Element.id")) 787 r = "string"; 788 if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Extension.url")) 789 r = "uri"; 790 String o = describeType(orig.getType().get(0)); 791 if (r == null && o == null) 792 System.out.println("null @ " + rev.getPath()); 793 if (r.contains("(") && o.contains("(") && r.startsWith(o.substring(0, o.indexOf("(") + 1))) { 794 compareParameters(bp, rev.getType().get(0), orig.getType().get(0)); 795 } else if (!r.equals(o)) 796 bp.append("Type changed from " + o + " to " + r); 797 } else { 798 CommaSeparatedStringBuilder removed = new CommaSeparatedStringBuilder(); 799 CommaSeparatedStringBuilder added = new CommaSeparatedStringBuilder(); 800 CommaSeparatedStringBuilder retargetted = new CommaSeparatedStringBuilder(); 801 for (TypeRefComponent tr : orig.getType()) { 802 if (!hasType(rev.getType(), tr)) 803 removed.append(describeType(tr)); 804 } 805 for (TypeRefComponent tr : rev.getType()) { 806 if (!hasType(orig.getType(), tr) && !isAbstractType(tr.getWorkingCode())) 807 added.append(describeType(tr)); 808 } 809 for (TypeRefComponent tr : rev.getType()) { 810 TypeRefComponent tm = getType(rev.getType(), tr); 811 if (tm != null) { 812 compareParameters(bp, tr, tm); 813 } 814 } 815 if (added.length() > 0) 816 bp.append("Add " + Utilities.pluralize("Type", added.count()) + " " + added); 817 if (removed.length() > 0) 818 bp.append("Remove " + Utilities.pluralize("Type", removed.count()) + " " + removed); 819 if (retargetted.length() > 0) 820 bp.append(retargetted.toString()); 821 } 822 } 823 824 private void compareParameters(CommaSeparatedStringBuilder bp, TypeRefComponent tr, TypeRefComponent tm) { 825 List<String> added = new ArrayList<>(); 826 List<String> removed = new ArrayList<>(); 827 828 for (CanonicalType p : tr.getTargetProfile()) { 829 if (!hasParam(tm, p.asStringValue())) { 830 added.add(trimNS(p.asStringValue())); 831 } 832 } 833 834 for (CanonicalType p : tm.getTargetProfile()) { 835 if (!hasParam(tr, p.asStringValue())) { 836 removed.add(trimNS(p.asStringValue())); 837 } 838 } 839 840 if (!added.isEmpty()) 841 bp.append("Type " + tr.getWorkingCode() + ": Added Target " + Utilities.pluralize("Type", added.size()) + " " + csv(added)); 842 if (!removed.isEmpty()) 843 bp.append("Type " + tr.getWorkingCode() + ": Removed Target " + Utilities.pluralize("Type", removed.size()) + " " + csv(removed)); 844 } 845 846 private String trimNS(String v) { 847 if (v.startsWith("http://hl7.org/fhir/StructureDefinition/")) 848 return v.substring(40); 849 return v; 850 } 851 852 private String csv(List<String> list) { 853 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 854 for (String s : list) 855 b.append(s); 856 return b.toString(); 857 } 858 859 private boolean hasParam(TypeRefComponent tm, String s) { 860 for (CanonicalType t : tm.getTargetProfile()) 861 if (s.equals(t.asStringValue())) 862 return true; 863 return false; 864 } 865 866 private boolean isAbstractType(String code) { 867 return Utilities.existsInList(code, "Element", "BackboneElement"); 868 } 869 870 private boolean hasType(List<TypeRefComponent> types, TypeRefComponent tr) { 871 for (TypeRefComponent t : types) { 872 if (t.getWorkingCode().equals(tr.getWorkingCode())) { 873 if (((!t.hasProfile() && !tr.hasProfile()) || (t.getProfile().equals(tr.getProfile())))) 874 return true; 875 } 876 } 877 return false; 878 } 879 880 private TypeRefComponent getType(List<TypeRefComponent> types, TypeRefComponent tr) { 881 for (TypeRefComponent t : types) { 882 if (t.getWorkingCode().equals(tr.getWorkingCode())) { 883 return t; 884 } 885 } 886 return null; 887 } 888 889 private String describeType(TypeRefComponent tr) { 890 if (!tr.hasProfile() && !tr.hasTargetProfile()) 891 return tr.getWorkingCode(); 892 else if (Utilities.existsInList(tr.getWorkingCode(), "Reference", "canonical")) { 893 StringBuilder b = new StringBuilder(tr.getWorkingCode()); 894 b.append("("); 895 boolean first = true; 896 for (UriType u : tr.getTargetProfile()) { 897 if (first) 898 first = false; 899 else 900 b.append(" | "); 901 if (u.getValue().startsWith("http://hl7.org/fhir/StructureDefinition/")) 902 b.append(u.getValue().substring(40)); 903 else 904 b.append(u.getValue()); 905 } 906 b.append(")"); 907 return b.toString(); 908 } else { 909 StringBuilder b = new StringBuilder(tr.getWorkingCode()); 910 if (tr.getProfile().size() > 0) { 911 b.append("("); 912 boolean first = true; 913 for (UriType u : tr.getTargetProfile()) { 914 if (first) 915 first = false; 916 else 917 b.append(" | "); 918 b.append(u.getValue()); 919 } 920 b.append(")"); 921 } 922 return b.toString(); 923 } 924 } 925 926 public void saveR4AsR5(ZipGenerator zip, FhirFormat fmt) throws IOException { 927 for (StructureDefinition t : original.getTypes().values()) 928 saveResource(zip, t, fmt); 929 for (StructureDefinition t : original.getResources().values()) 930 saveResource(zip, t, fmt); 931 for (StructureDefinition t : original.getProfiles().values()) 932 saveResource(zip, t, fmt); 933 for (StructureDefinition t : original.getExtensions().values()) 934 saveResource(zip, t, fmt); 935 for (ValueSet t : original.getValuesets().values()) 936 saveResource(zip, t, fmt); 937 for (ValueSet t : original.getExpansions().values()) 938 saveResource(zip, t, fmt); 939 } 940 941 private void saveResource(ZipGenerator zip, Resource t, FhirFormat fmt) throws IOException { 942 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 943 if (fmt == FhirFormat.JSON) 944 new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(bs, t); 945 else 946 new XmlParser().setOutputStyle(OutputStyle.PRETTY).compose(bs, t); 947 zip.addBytes(t.fhirType() + "-" + t.getId() + "." + fmt.getExtension(), bs.toByteArray(), true); 948 } 949 950 private void compareJson(JsonObject type, StructureDefinition orig, StructureDefinition rev) { 951 JsonObject elements = new JsonObject(); 952 // first, we must match revision elements to old elements 953 boolean changed = false; 954 if (!orig.getName().equals(rev.getName())) { 955 changed = true; 956 type.addProperty("old-name", orig.getName()); 957 } 958 for (ElementDefinition ed : rev.getDifferential().getElement()) { 959 ElementDefinition oed = getMatchingElement(rev.getName(), orig.getDifferential().getElement(), ed); 960 if (oed != null) { 961 ed.setUserData("match", oed); 962 oed.setUserData("match", ed); 963 } 964 } 965 966 for (ElementDefinition ed : rev.getDifferential().getElement()) { 967 ElementDefinition oed = (ElementDefinition) ed.getUserData("match"); 968 if (oed == null) { 969 changed = true; 970 JsonObject element = new JsonObject(); 971 elements.add(ed.getPath(), element); 972 element.addProperty("status", "new"); 973 } else 974 changed = compareElementJson(elements, ed, oed) || changed; 975 } 976 977 List<String> dels = new ArrayList<String>(); 978 979 for (ElementDefinition ed : orig.getDifferential().getElement()) { 980 if (ed.getUserData("match") == null) { 981 changed = true; 982 boolean marked = false; 983 for (String s : dels) 984 if (ed.getPath().startsWith(s + ".")) 985 marked = true; 986 if (!marked) { 987 dels.add(ed.getPath()); 988 JsonObject element = new JsonObject(); 989 elements.add(ed.getPath(), element); 990 element.addProperty("status", "deleted"); 991 } 992 } 993 } 994 995 if (elements.entrySet().size() > 0) 996 type.add("elements", elements); 997 998 if (changed) 999 type.addProperty("status", "changed"); 1000 else 1001 type.addProperty("status", "no-change"); 1002 1003 for (ElementDefinition ed : rev.getDifferential().getElement()) 1004 ed.clearUserData("match"); 1005 for (ElementDefinition ed : orig.getDifferential().getElement()) 1006 ed.clearUserData("match"); 1007 1008 } 1009 1010 private void compareXml(Document doc, Element type, StructureDefinition orig, StructureDefinition rev) { 1011 // first, we must match revision elements to old elements 1012 boolean changed = false; 1013 if (!orig.getName().equals(rev.getName())) { 1014 changed = true; 1015 type.setAttribute("old-name", orig.getName()); 1016 } 1017 for (ElementDefinition ed : rev.getDifferential().getElement()) { 1018 ElementDefinition oed = getMatchingElement(rev.getName(), orig.getDifferential().getElement(), ed); 1019 if (oed != null) { 1020 ed.setUserData("match", oed); 1021 oed.setUserData("match", ed); 1022 } 1023 } 1024 1025 for (ElementDefinition ed : rev.getDifferential().getElement()) { 1026 ElementDefinition oed = (ElementDefinition) ed.getUserData("match"); 1027 if (oed == null) { 1028 changed = true; 1029 Element element = doc.createElement("element"); 1030 element.setAttribute("path", ed.getPath()); 1031 type.appendChild(element); 1032 element.setAttribute("status", "new"); 1033 } else 1034 changed = compareElementXml(doc, type, ed, oed) || changed; 1035 } 1036 1037 List<String> dels = new ArrayList<String>(); 1038 1039 for (ElementDefinition ed : orig.getDifferential().getElement()) { 1040 if (ed.getUserData("match") == null) { 1041 changed = true; 1042 boolean marked = false; 1043 for (String s : dels) 1044 if (ed.getPath().startsWith(s + ".")) 1045 marked = true; 1046 if (!marked) { 1047 dels.add(ed.getPath()); 1048 Element element = doc.createElement("element"); 1049 element.setAttribute("path", ed.getPath()); 1050 type.appendChild(element); 1051 element.setAttribute("status", "deleted"); 1052 } 1053 } 1054 } 1055 1056 if (changed) 1057 type.setAttribute("status", "changed"); 1058 else 1059 type.setAttribute("status", "no-change"); 1060 1061 for (ElementDefinition ed : rev.getDifferential().getElement()) 1062 ed.clearUserData("match"); 1063 for (ElementDefinition ed : orig.getDifferential().getElement()) 1064 ed.clearUserData("match"); 1065 1066 } 1067 1068 private boolean compareElementJson(JsonObject elements, ElementDefinition rev, ElementDefinition orig) { 1069 JsonObject element = new JsonObject(); 1070 1071 String rn = tail(rev.getPath()); 1072 String on = tail(orig.getPath()); 1073 1074 if (!rn.equals(on) && rev.getPath().contains(".")) 1075 element.addProperty("old-name", on); 1076 1077 if (rev.getMin() != orig.getMin()) { 1078 element.addProperty("old-min", orig.getMin()); 1079 element.addProperty("new-min", rev.getMin()); 1080 } 1081 1082 if (!rev.getMax().equals(orig.getMax())) { 1083 element.addProperty("old-max", orig.getMax()); 1084 element.addProperty("new-max", rev.getMax()); 1085 } 1086 1087 analyseTypes(element, rev, orig); 1088 1089 if (hasBindingToNote(rev) || hasBindingToNote(orig)) { 1090 compareBindings(element, rev, orig); 1091 } 1092 1093 if (rev.hasDefaultValue() || orig.hasDefaultValue()) { 1094 boolean changed = true; 1095 if (!rev.hasDefaultValue()) 1096 element.addProperty("default", "removed"); 1097 else if (!orig.hasDefaultValue()) 1098 element.addProperty("default", "added"); 1099 else { 1100 String s1 = describeValue(orig.getDefaultValue()); 1101 String s2 = describeValue(rev.getDefaultValue()); 1102 if (!s1.equals(s2)) 1103 element.addProperty("default", "changed"); 1104 else 1105 changed = false; 1106 } 1107 if (changed) { 1108 if (orig.hasDefaultValue()) 1109 element.addProperty("old-default", describeValue(orig.getDefaultValue())); 1110 if (rev.hasDefaultValue()) 1111 element.addProperty("new-default", describeValue(rev.getDefaultValue())); 1112 } 1113 } 1114 1115 if (rev.getIsModifier() != orig.getIsModifier()) { 1116 if (rev.getIsModifier()) 1117 element.addProperty("modifier", "added"); 1118 else 1119 element.addProperty("modifier", "removed"); 1120 } 1121 1122 if (element.entrySet().isEmpty()) 1123 return false; 1124 else { 1125 elements.add(rev.getPath(), element); 1126 return true; 1127 } 1128 } 1129 1130 private boolean compareElementXml(Document doc, Element type, ElementDefinition rev, ElementDefinition orig) { 1131 Element element = doc.createElement("element"); 1132 1133 String rn = tail(rev.getPath()); 1134 String on = tail(orig.getPath()); 1135 1136 if (!rn.equals(on) && rev.getPath().contains(".")) 1137 element.setAttribute("old-name", on); 1138 1139 if (rev.getMin() != orig.getMin()) { 1140 element.setAttribute("old-min", Integer.toString(orig.getMin())); 1141 element.setAttribute("new-min", Integer.toString(rev.getMin())); 1142 } 1143 1144 if (!rev.getMax().equals(orig.getMax())) { 1145 element.setAttribute("old-max", orig.getMax()); 1146 element.setAttribute("new-max", rev.getMax()); 1147 } 1148 1149 analyseTypes(doc, element, rev, orig); 1150 1151 if (hasBindingToNote(rev) || hasBindingToNote(orig)) { 1152 compareBindings(doc, element, rev, orig); 1153 } 1154 1155 if (rev.hasDefaultValue() || orig.hasDefaultValue()) { 1156 boolean changed = true; 1157 if (!rev.hasDefaultValue()) 1158 element.setAttribute("default", "removed"); 1159 else if (!orig.hasDefaultValue()) 1160 element.setAttribute("default", "added"); 1161 else { 1162 String s1 = describeValue(orig.getDefaultValue()); 1163 String s2 = describeValue(rev.getDefaultValue()); 1164 if (!s1.equals(s2)) 1165 element.setAttribute("default", "changed"); 1166 else 1167 changed = false; 1168 } 1169 if (changed) { 1170 if (orig.hasDefaultValue()) 1171 element.setAttribute("old-default", describeValue(orig.getDefaultValue())); 1172 if (rev.hasDefaultValue()) 1173 element.setAttribute("new-default", describeValue(rev.getDefaultValue())); 1174 } 1175 } 1176 1177 if (rev.getIsModifier() != orig.getIsModifier()) { 1178 if (rev.getIsModifier()) 1179 element.setAttribute("modifier", "added"); 1180 else 1181 element.setAttribute("modifier", "removed"); 1182 } 1183 1184 if (element.getAttributes().getLength() == 0 && element.getChildNodes().getLength() == 0) 1185 return false; 1186 else { 1187 element.setAttribute("path", rev.getPath()); 1188 type.appendChild(element); 1189 return true; 1190 } 1191 } 1192 1193 private void analyseTypes(JsonObject element, ElementDefinition rev, ElementDefinition orig) { 1194 JsonArray oa = new JsonArray(); 1195 JsonArray ra = new JsonArray(); 1196 1197 if (rev.getType().size() == 1 && orig.getType().size() == 1) { 1198 String r = describeType(rev.getType().get(0)); 1199 if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Element.id", "Extension.url")) 1200 r = "string"; 1201 String o = describeType(orig.getType().get(0)); 1202 if (Utilities.noString(o) && Utilities.existsInList(orig.getId(), "Element.id", "Extension.url")) 1203 o = "string"; 1204 if (!o.equals(r)) { 1205 oa.add(new JsonPrimitive(o)); 1206 ra.add(new JsonPrimitive(r)); 1207 } 1208 } else { 1209 for (TypeRefComponent tr : orig.getType()) { 1210 if (!hasType(rev.getType(), tr)) 1211 oa.add(new JsonPrimitive(describeType(tr))); 1212 } 1213 for (TypeRefComponent tr : rev.getType()) { 1214 if (!hasType(orig.getType(), tr) && !isAbstractType(tr.getWorkingCode())) 1215 ra.add(new JsonPrimitive(describeType(tr))); 1216 } 1217 for (TypeRefComponent tr : rev.getType()) { 1218 TypeRefComponent tm = getType(rev.getType(), tr); 1219 if (tm != null) { 1220 compareParameters(element, tr, tm); 1221 } 1222 } 1223 1224 } 1225 if (oa.size() > 0) 1226 element.add("removed-types", oa); 1227 if (ra.size() > 0) 1228 element.add("added-types", ra); 1229 } 1230 1231 private void compareParameters(JsonObject element, TypeRefComponent tr, TypeRefComponent tm) { 1232 JsonArray added = new JsonArray(); 1233 JsonArray removed = new JsonArray(); 1234 1235 for (CanonicalType p : tr.getTargetProfile()) { 1236 if (!hasParam(tm, p.asStringValue())) { 1237 added.add(new JsonPrimitive(p.asStringValue())); 1238 } 1239 } 1240 1241 for (CanonicalType p : tm.getTargetProfile()) { 1242 if (!hasParam(tr, p.asStringValue())) { 1243 removed.add(new JsonPrimitive(p.asStringValue())); 1244 } 1245 } 1246 1247 if (added.size() > 0) 1248 element.add(tr.getWorkingCode() + "-target-added", added); 1249 if (removed.size() > 0) 1250 element.add(tr.getWorkingCode() + "-target-removed", removed); 1251 } 1252 1253 private void analyseTypes(Document doc, Element element, ElementDefinition rev, ElementDefinition orig) { 1254 if (rev.getType().size() == 1 && orig.getType().size() == 1) { 1255 String r = describeType(rev.getType().get(0)); 1256 if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Element.id", "Extension.url")) 1257 r = "string"; 1258 String o = describeType(orig.getType().get(0)); 1259 if (Utilities.noString(o) && Utilities.existsInList(orig.getId(), "Element.id", "Extension.url")) 1260 o = "string"; 1261 if (!o.equals(r)) { 1262 element.appendChild(makeElementWithAttribute(doc, "removed-type", "name", o)); 1263 element.appendChild(makeElementWithAttribute(doc, "added-type", "name", r)); 1264 } 1265 } else { 1266 for (TypeRefComponent tr : orig.getType()) { 1267 if (!hasType(rev.getType(), tr)) 1268 element.appendChild(makeElementWithAttribute(doc, "removed-type", "name", describeType(tr))); 1269 } 1270 for (TypeRefComponent tr : rev.getType()) { 1271 if (!hasType(orig.getType(), tr) && !isAbstractType(tr.getWorkingCode())) 1272 element.appendChild(makeElementWithAttribute(doc, "added-type", "name", describeType(tr))); 1273 } 1274 for (TypeRefComponent tr : rev.getType()) { 1275 TypeRefComponent tm = getType(rev.getType(), tr); 1276 if (tm != null) { 1277 compareParameters(doc, element, tr, tm); 1278 } 1279 } 1280 } 1281 } 1282 1283 private void compareParameters(Document doc, Element element, TypeRefComponent tr, TypeRefComponent tm) { 1284 1285 for (CanonicalType p : tr.getTargetProfile()) { 1286 if (!hasParam(tm, p.asStringValue())) { 1287 element.appendChild(makeElementWithAttribute(doc, tr.getWorkingCode() + "-target-added", "name", p.asStringValue())); 1288 } 1289 } 1290 1291 for (CanonicalType p : tm.getTargetProfile()) { 1292 if (!hasParam(tr, p.asStringValue())) { 1293 element.appendChild(makeElementWithAttribute(doc, tr.getWorkingCode() + "-target-removed", "name", p.asStringValue())); 1294 } 1295 } 1296 } 1297 1298 private Node makeElementWithAttribute(Document doc, String name, String aname, String content) { 1299 Element e = doc.createElement(name); 1300 e.setAttribute(aname, content); 1301 return e; 1302 } 1303 1304 private void compareBindings(JsonObject element, ElementDefinition rev, ElementDefinition orig) { 1305 if (!hasBindingToNote(rev)) { 1306 element.addProperty("binding-status", "removed"); 1307 describeBinding(element, "old-binding", orig); 1308 } else if (!hasBindingToNote(orig)) { 1309 element.addProperty("binding-status", "added"); 1310 describeBinding(element, "new-binding", rev); 1311 } else if (compareBindings(element, rev.getBinding(), orig.getBinding())) { 1312 element.addProperty("binding-status", "changed"); 1313 describeBinding(element, "old-binding", orig); 1314 describeBinding(element, "new-binding", rev); 1315 } 1316 } 1317 1318 private boolean compareBindings(JsonObject element, ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) { 1319 boolean res = false; 1320 if (rev.getStrength() != orig.getStrength()) { 1321 element.addProperty("binding-strength-changed", true); 1322 res = true; 1323 } 1324 if (!Base.compareDeep(rev.getValueSet(), orig.getValueSet(), false)) { 1325 element.addProperty("binding-valueset-changed", true); 1326 res = true; 1327 } 1328 if (!maxValueSetsMatch(rev, orig)) { 1329 element.addProperty("max-valueset-changed", true); 1330 res = true; 1331 } 1332 1333 if (rev.getStrength() == BindingStrength.REQUIRED && orig.getStrength() == BindingStrength.REQUIRED) { 1334 JsonArray oa = new JsonArray(); 1335 JsonArray ra = new JsonArray(); 1336 ValueSet vrev = getValueSet(rev.getValueSet(), revision.getExpansions()); 1337 ValueSet vorig = getValueSet(rev.getValueSet(), original.getExpansions()); 1338 if (vrev != null && vorig != null) { 1339 for (ValueSetExpansionContainsComponent cc : vorig.getExpansion().getContains()) { 1340 if (!hasCode(vrev, cc)) 1341 oa.add(new JsonPrimitive(cc.getCode())); 1342 } 1343 for (ValueSetExpansionContainsComponent cc : vrev.getExpansion().getContains()) { 1344 if (!hasCode(vorig, cc)) 1345 ra.add(new JsonPrimitive(cc.getCode())); 1346 } 1347 } 1348 if (oa.size() > 0 || ra.size() > 0) { 1349 element.addProperty("binding-codes-changed", true); 1350 res = true; 1351 } 1352 if (oa.size() > 0) 1353 element.add("removed-codes", oa); 1354 if (ra.size() > 0) 1355 element.add("added-codes", ra); 1356 } 1357 return res; 1358 } 1359 1360 private boolean hasCode(ValueSet vs, ValueSetExpansionContainsComponent cc) { 1361 for (ValueSetExpansionContainsComponent ct : vs.getExpansion().getContains()) { 1362 if (ct.getSystem().equals(cc.getSystem()) && ct.getCode().equals(cc.getCode())) 1363 return true; 1364 } 1365 return false; 1366 } 1367 1368 private void compareBindings(Document doc, Element element, ElementDefinition rev, ElementDefinition orig) { 1369 if (!hasBindingToNote(rev)) { 1370 element.setAttribute("binding-status", "removed"); 1371 describeBinding(doc, element, "old-binding", orig); 1372 } else if (!hasBindingToNote(orig)) { 1373 element.setAttribute("binding-status", "added"); 1374 describeBinding(doc, element, "new-binding", rev); 1375 } else if (compareBindings(doc, element, rev.getBinding(), orig.getBinding())) { 1376 element.setAttribute("binding-status", "changed"); 1377 describeBinding(doc, element, "old-binding", orig); 1378 describeBinding(doc, element, "new-binding", rev); 1379 } 1380 } 1381 1382 private boolean compareBindings(Document doc, Element element, ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) { 1383 boolean res = false; 1384 if (rev.getStrength() != orig.getStrength()) { 1385 element.setAttribute("binding-strength-changed", "true"); 1386 res = true; 1387 } 1388 if (!Base.compareDeep(rev.getValueSet(), orig.getValueSet(), false)) { 1389 element.setAttribute("binding-valueset-changed", "true"); 1390 res = true; 1391 } 1392 if (!maxValueSetsMatch(rev, orig)) { 1393 element.setAttribute("max-valueset-changed", "true"); 1394 res = true; 1395 } 1396 if (rev.getStrength() == BindingStrength.REQUIRED && orig.getStrength() == BindingStrength.REQUIRED) { 1397 ValueSet vrev = getValueSet(rev.getValueSet(), revision.getExpansions()); 1398 ValueSet vorig = getValueSet(rev.getValueSet(), original.getExpansions()); 1399 boolean changed = false; 1400 if (vrev != null && vorig != null) { 1401 for (ValueSetExpansionContainsComponent cc : vorig.getExpansion().getContains()) { 1402 if (!hasCode(vrev, cc)) { 1403 element.appendChild(makeElementWithAttribute(doc, "removed-code", "code", cc.getCode())); 1404 changed = true; 1405 } 1406 } 1407 for (ValueSetExpansionContainsComponent cc : vrev.getExpansion().getContains()) { 1408 if (!hasCode(vorig, cc)) { 1409 element.appendChild(makeElementWithAttribute(doc, "added-code", "code", cc.getCode())); 1410 changed = true; 1411 } 1412 } 1413 } 1414 if (changed) { 1415 element.setAttribute("binding-codes-changed", "true"); 1416 res = true; 1417 } 1418 } 1419 return res; 1420 } 1421}