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