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