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