001package org.hl7.fhir.r4.conformance; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032import java.io.File; 033import java.io.IOException; 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.Collections; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040 041import org.hl7.fhir.exceptions.DefinitionException; 042import org.hl7.fhir.exceptions.FHIRFormatError; 043import org.hl7.fhir.r4.context.IWorkerContext; 044import org.hl7.fhir.r4.formats.IParser; 045import org.hl7.fhir.r4.model.Base; 046import org.hl7.fhir.r4.model.Coding; 047import org.hl7.fhir.r4.model.ElementDefinition; 048import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent; 049import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionConstraintComponent; 050import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionMappingComponent; 051import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent; 052import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; 053import org.hl7.fhir.r4.model.Enumerations.BindingStrength; 054import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; 055import org.hl7.fhir.r4.model.IntegerType; 056import org.hl7.fhir.r4.model.PrimitiveType; 057import org.hl7.fhir.r4.model.StringType; 058import org.hl7.fhir.r4.model.StructureDefinition; 059import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; 060import org.hl7.fhir.r4.model.Type; 061import org.hl7.fhir.r4.model.ValueSet; 062import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent; 063import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 064import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 065import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 066import org.hl7.fhir.r4.utils.DefinitionNavigator; 067import org.hl7.fhir.r4.utils.ToolingExtensions; 068import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 069import org.hl7.fhir.utilities.TextFile; 070import org.hl7.fhir.utilities.Utilities; 071import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 072import org.hl7.fhir.utilities.http.HTTPResult; 073import org.hl7.fhir.utilities.http.ManagedWebAccess; 074import org.hl7.fhir.utilities.validation.ValidationMessage; 075import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 076 077/** 078 * A engine that generates difference analysis between two sets of structure 079 * definitions, typically from 2 different implementation guides. 080 * 081 * How this class works is that you create it with access to a bunch of 082 * underying resources that includes all the structure definitions from both 083 * implementation guides 084 * 085 * Once the class is created, you repeatedly pass pairs of structure 086 * definitions, one from each IG, building up a web of difference analyses. This 087 * class will automatically process any internal comparisons that it encounters 088 * 089 * When all the comparisons have been performed, you can then generate a variety 090 * of output formats 091 * 092 * @author Grahame Grieve 093 * 094 */ 095public class ProfileComparer { 096 097 private IWorkerContext context; 098 099 public ProfileComparer(IWorkerContext context) { 100 super(); 101 this.context = context; 102 } 103 104 private static final int BOTH_NULL = 0; 105 private static final int EITHER_NULL = 1; 106 107 public class ProfileComparison { 108 private String id; 109 /** 110 * the first of two structures that were compared to generate this comparison 111 * 112 * In a few cases - selection of example content and value sets - left gets 113 * preference over right 114 */ 115 private StructureDefinition left; 116 117 /** 118 * the second of two structures that were compared to generate this comparison 119 * 120 * In a few cases - selection of example content and value sets - left gets 121 * preference over right 122 */ 123 private StructureDefinition right; 124 125 public String getId() { 126 return id; 127 } 128 129 private String leftName() { 130 return left.getName(); 131 } 132 133 private String rightName() { 134 return right.getName(); 135 } 136 137 /** 138 * messages generated during the comparison. There are 4 grades of messages: 139 * information - a list of differences between structures warnings - notifies 140 * that the comparer is unable to fully compare the structures (constraints 141 * differ, open value sets) errors - where the structures are incompatible fatal 142 * errors - some error that prevented full analysis 143 * 144 * @return 145 */ 146 private List<ValidationMessage> messages = new ArrayList<ValidationMessage>(); 147 148 /** 149 * The structure that describes all instances that will conform to both 150 * structures 151 */ 152 private StructureDefinition subset; 153 154 /** 155 * The structure that describes all instances that will conform to either 156 * structures 157 */ 158 private StructureDefinition superset; 159 160 public StructureDefinition getLeft() { 161 return left; 162 } 163 164 public StructureDefinition getRight() { 165 return right; 166 } 167 168 public List<ValidationMessage> getMessages() { 169 return messages; 170 } 171 172 public StructureDefinition getSubset() { 173 return subset; 174 } 175 176 public StructureDefinition getSuperset() { 177 return superset; 178 } 179 180 private boolean ruleEqual(String path, ElementDefinition ed, String vLeft, String vRight, String description, 181 boolean nullOK) { 182 if (vLeft == null && vRight == null && nullOK) 183 return true; 184 if (vLeft == null && vRight == null) { 185 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 186 description + " and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); 187 if (ed != null) 188 status(ed, ProfileUtilities.STATUS_ERROR); 189 } 190 if (vLeft == null || !vLeft.equals(vRight)) { 191 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 192 description + " (" + vLeft + "/" + vRight + ")", ValidationMessage.IssueSeverity.ERROR)); 193 if (ed != null) 194 status(ed, ProfileUtilities.STATUS_ERROR); 195 } 196 return true; 197 } 198 199 private boolean ruleCompares(ElementDefinition ed, Type vLeft, Type vRight, String path, int nullStatus) 200 throws IOException { 201 if (vLeft == null && vRight == null && nullStatus == BOTH_NULL) 202 return true; 203 if (vLeft == null && vRight == null) { 204 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 205 "Must be the same and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); 206 status(ed, ProfileUtilities.STATUS_ERROR); 207 } 208 if (vLeft == null && nullStatus == EITHER_NULL) 209 return true; 210 if (vRight == null && nullStatus == EITHER_NULL) 211 return true; 212 if (vLeft == null || vRight == null || !Base.compareDeep(vLeft, vRight, false)) { 213 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 214 "Must be the same (" + toString(vLeft) + "/" + toString(vRight) + ")", 215 ValidationMessage.IssueSeverity.ERROR)); 216 status(ed, ProfileUtilities.STATUS_ERROR); 217 } 218 return true; 219 } 220 221 private boolean rule(ElementDefinition ed, boolean test, String path, String message) { 222 if (!test) { 223 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, message, 224 ValidationMessage.IssueSeverity.ERROR)); 225 status(ed, ProfileUtilities.STATUS_ERROR); 226 } 227 return test; 228 } 229 230 private boolean ruleEqual(ElementDefinition ed, boolean vLeft, boolean vRight, String path, String elementName) { 231 if (vLeft != vRight) { 232 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 233 elementName + " must be the same (" + vLeft + "/" + vRight + ")", ValidationMessage.IssueSeverity.ERROR)); 234 status(ed, ProfileUtilities.STATUS_ERROR); 235 } 236 return true; 237 } 238 239 private String toString(Type val) throws IOException { 240 if (val instanceof PrimitiveType) 241 return "\"" + ((PrimitiveType) val).getValueAsString() + "\""; 242 243 IParser jp = context.newJsonParser(); 244 return jp.composeString(val, "value"); 245 } 246 247 public String getErrorCount() { 248 int c = 0; 249 for (ValidationMessage vm : messages) 250 if (vm.getLevel() == ValidationMessage.IssueSeverity.ERROR) 251 c++; 252 return Integer.toString(c); 253 } 254 255 public String getWarningCount() { 256 int c = 0; 257 for (ValidationMessage vm : messages) 258 if (vm.getLevel() == ValidationMessage.IssueSeverity.WARNING) 259 c++; 260 return Integer.toString(c); 261 } 262 263 public String getHintCount() { 264 int c = 0; 265 for (ValidationMessage vm : messages) 266 if (vm.getLevel() == ValidationMessage.IssueSeverity.INFORMATION) 267 c++; 268 return Integer.toString(c); 269 } 270 } 271 272 /** 273 * Value sets used in the subset and superset 274 */ 275 private List<ValueSet> valuesets = new ArrayList<ValueSet>(); 276 private List<ProfileComparison> comparisons = new ArrayList<ProfileComparison>(); 277 private String id; 278 private String title; 279 private String leftLink; 280 private String leftName; 281 private String rightLink; 282 private String rightName; 283 284 public List<ValueSet> getValuesets() { 285 return valuesets; 286 } 287 288 public void status(ElementDefinition ed, int value) { 289 ed.setUserData(ProfileUtilities.UD_ERROR_STATUS, Math.max(value, ed.getUserInt("error-status"))); 290 } 291 292 public List<ProfileComparison> getComparisons() { 293 return comparisons; 294 } 295 296 /** 297 * Compare left and right structure definitions to see whether they are 298 * consistent or not 299 * 300 * Note that left and right are arbitrary choices. In one respect, left is 301 * 'preferred' - the left's example value and data sets will be selected over 302 * the right ones in the common structure definition 303 * 304 * @throws DefinitionException 305 * @throws IOException 306 * @throws FHIRFormatError 307 * 308 * @ 309 */ 310 public ProfileComparison compareProfiles(StructureDefinition left, StructureDefinition right) 311 throws DefinitionException, IOException, FHIRFormatError { 312 ProfileComparison outcome = new ProfileComparison(); 313 outcome.left = left; 314 outcome.right = right; 315 316 if (left == null) 317 throw new DefinitionException("No StructureDefinition provided (left)"); 318 if (right == null) 319 throw new DefinitionException("No StructureDefinition provided (right)"); 320 if (!left.hasSnapshot()) 321 throw new DefinitionException("StructureDefinition has no snapshot (left: " + outcome.leftName() + ")"); 322 if (!right.hasSnapshot()) 323 throw new DefinitionException("StructureDefinition has no snapshot (right: " + outcome.rightName() + ")"); 324 if (left.getSnapshot().getElement().isEmpty()) 325 throw new DefinitionException("StructureDefinition snapshot is empty (left: " + outcome.leftName() + ")"); 326 if (right.getSnapshot().getElement().isEmpty()) 327 throw new DefinitionException("StructureDefinition snapshot is empty (right: " + outcome.rightName() + ")"); 328 329 for (ProfileComparison pc : comparisons) 330 if (pc.left.getUrl().equals(left.getUrl()) && pc.right.getUrl().equals(right.getUrl())) 331 return pc; 332 333 outcome.id = Integer.toString(comparisons.size() + 1); 334 comparisons.add(outcome); 335 336 DefinitionNavigator ln = new DefinitionNavigator(context, left); 337 DefinitionNavigator rn = new DefinitionNavigator(context, right); 338 339 // from here on in, any issues go in messages 340 outcome.superset = new StructureDefinition(); 341 outcome.subset = new StructureDefinition(); 342 if (outcome.ruleEqual(ln.path(), null, ln.path(), rn.path(), "Base Type is not compatible", false)) { 343 if (compareElements(outcome, ln.path(), ln, rn)) { 344 outcome.subset.setName("intersection of " + outcome.leftName() + " and " + outcome.rightName()); 345 outcome.subset.setStatus(PublicationStatus.DRAFT); 346 outcome.subset.setKind(outcome.left.getKind()); 347 outcome.subset.setType(outcome.left.getType()); 348 outcome.subset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/" + outcome.subset.getType()); 349 outcome.subset.setDerivation(TypeDerivationRule.CONSTRAINT); 350 outcome.subset.setAbstract(false); 351 outcome.superset.setName("union of " + outcome.leftName() + " and " + outcome.rightName()); 352 outcome.superset.setStatus(PublicationStatus.DRAFT); 353 outcome.superset.setKind(outcome.left.getKind()); 354 outcome.superset.setType(outcome.left.getType()); 355 outcome.superset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/" + outcome.subset.getType()); 356 outcome.superset.setAbstract(false); 357 outcome.superset.setDerivation(TypeDerivationRule.CONSTRAINT); 358 } else { 359 outcome.subset = null; 360 outcome.superset = null; 361 } 362 } 363 return outcome; 364 } 365 366 /** 367 * left and right refer to the same element. Are they compatible? 368 * 369 * @param outcome 370 * @param outcome 371 * @param path 372 * @param left 373 * @param right @- if there's a problem that needs fixing in this code 374 * @throws DefinitionException 375 * @throws IOException 376 * @throws FHIRFormatError 377 */ 378 private boolean compareElements(ProfileComparison outcome, String path, DefinitionNavigator left, 379 DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { 380// preconditions: 381 assert (path != null); 382 assert (left != null); 383 assert (right != null); 384 assert (left.path().equals(right.path())); 385 386 // we ignore slicing right now - we're going to clone the root one anyway, and 387 // then think about clones 388 // simple stuff 389 ElementDefinition subset = new ElementDefinition(); 390 subset.setPath(left.path()); 391 392 // not allowed to be different: 393 subset.getRepresentation().addAll(left.current().getRepresentation()); // can't be bothered even testing this one 394 if (!outcome.ruleCompares(subset, left.current().getDefaultValue(), right.current().getDefaultValue(), 395 path + ".defaultValue[x]", BOTH_NULL)) 396 return false; 397 subset.setDefaultValue(left.current().getDefaultValue()); 398 if (!outcome.ruleEqual(path, subset, left.current().getMeaningWhenMissing(), 399 right.current().getMeaningWhenMissing(), "meaningWhenMissing Must be the same", true)) 400 return false; 401 subset.setMeaningWhenMissing(left.current().getMeaningWhenMissing()); 402 if (!outcome.ruleEqual(subset, left.current().getIsModifier(), right.current().getIsModifier(), path, "isModifier")) 403 return false; 404 subset.setIsModifier(left.current().getIsModifier()); 405 if (!outcome.ruleEqual(subset, left.current().getIsSummary(), right.current().getIsSummary(), path, "isSummary")) 406 return false; 407 subset.setIsSummary(left.current().getIsSummary()); 408 409 // descriptive properties from ElementDefinition - merge them: 410 subset.setLabel(mergeText(subset, outcome, path, "label", left.current().getLabel(), right.current().getLabel())); 411 subset.setShort(mergeText(subset, outcome, path, "short", left.current().getShort(), right.current().getShort())); 412 subset.setDefinition(mergeText(subset, outcome, path, "definition", left.current().getDefinition(), 413 right.current().getDefinition())); 414 subset.setComment( 415 mergeText(subset, outcome, path, "comments", left.current().getComment(), right.current().getComment())); 416 subset.setRequirements(mergeText(subset, outcome, path, "requirements", left.current().getRequirements(), 417 right.current().getRequirements())); 418 subset.getCode().addAll(mergeCodings(left.current().getCode(), right.current().getCode())); 419 subset.getAlias().addAll(mergeStrings(left.current().getAlias(), right.current().getAlias())); 420 subset.getMapping().addAll(mergeMappings(left.current().getMapping(), right.current().getMapping())); 421 // left will win for example 422 subset.setExample(left.current().hasExample() ? left.current().getExample() : right.current().getExample()); 423 424 subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport()); 425 ElementDefinition superset = subset.copy(); 426 427 // compare and intersect 428 superset.setMin(unionMin(left.current().getMin(), right.current().getMin())); 429 superset.setMax(unionMax(left.current().getMax(), right.current().getMax())); 430 subset.setMin(intersectMin(left.current().getMin(), right.current().getMin())); 431 subset.setMax(intersectMax(left.current().getMax(), right.current().getMax())); 432 outcome.rule(subset, subset.getMax().equals("*") || Integer.parseInt(subset.getMax()) >= subset.getMin(), path, 433 "Cardinality Mismatch: " + card(left) + "/" + card(right)); 434 435 superset.getType().addAll(unionTypes(path, left.current().getType(), right.current().getType())); 436 subset.getType().addAll(intersectTypes(subset, outcome, path, left.current().getType(), right.current().getType())); 437 outcome.rule(subset, !subset.getType().isEmpty() || (!left.current().hasType() && !right.current().hasType()), path, 438 "Type Mismatch:\r\n " + typeCode(left) + "\r\n " + typeCode(right)); 439// <fixed[x]><!-- ?? 0..1 * Value must be exactly this --></fixed[x]> 440// <pattern[x]><!-- ?? 0..1 * Value must have at least these property values --></pattern[x]> 441 superset.setMaxLengthElement(unionMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); 442 subset.setMaxLengthElement(intersectMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); 443 if (left.current().hasBinding() || right.current().hasBinding()) { 444 compareBindings(outcome, subset, superset, path, left.current(), right.current()); 445 } 446 447 // note these are backwards 448 superset.getConstraint() 449 .addAll(intersectConstraints(path, left.current().getConstraint(), right.current().getConstraint())); 450 subset.getConstraint().addAll( 451 unionConstraints(subset, outcome, path, left.current().getConstraint(), right.current().getConstraint())); 452 453 // now process the slices 454 if (left.current().hasSlicing() || right.current().hasSlicing()) { 455 if (isExtension(left.path())) 456 return compareExtensions(outcome, path, superset, subset, left, right); 457// return true; 458 else { 459 ElementDefinitionSlicingComponent slicingL = left.current().getSlicing(); 460 ElementDefinitionSlicingComponent slicingR = right.current().getSlicing(); 461 throw new DefinitionException("Slicing is not handled yet"); 462 } 463 // todo: name 464 } 465 466 // add the children 467 outcome.subset.getSnapshot().getElement().add(subset); 468 outcome.superset.getSnapshot().getElement().add(superset); 469 return compareChildren(subset, outcome, path, left, right); 470 } 471 472 private class ExtensionUsage { 473 private DefinitionNavigator defn; 474 private int minSuperset; 475 private int minSubset; 476 private String maxSuperset; 477 private String maxSubset; 478 private boolean both = false; 479 480 public ExtensionUsage(DefinitionNavigator defn, int min, String max) { 481 super(); 482 this.defn = defn; 483 this.minSubset = min; 484 this.minSuperset = min; 485 this.maxSubset = max; 486 this.maxSuperset = max; 487 } 488 489 } 490 491 private boolean compareExtensions(ProfileComparison outcome, String path, ElementDefinition superset, 492 ElementDefinition subset, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException { 493 // for now, we don't handle sealed (or ordered) extensions 494 495 // for an extension the superset is all extensions, and the subset is.. all 496 // extensions - well, unless thay are sealed. 497 // but it's not useful to report that. instead, we collate the defined ones, and 498 // just adjust the cardinalities 499 Map<String, ExtensionUsage> map = new HashMap<String, ExtensionUsage>(); 500 501 if (left.slices() != null) 502 for (DefinitionNavigator ex : left.slices()) { 503 String url = ex.current().getType().get(0).getProfile().get(0).getValue(); 504 if (map.containsKey(url)) 505 throw new DefinitionException("Duplicate Extension " + url + " at " + path); 506 else 507 map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax())); 508 } 509 if (right.slices() != null) 510 for (DefinitionNavigator ex : right.slices()) { 511 String url = ex.current().getType().get(0).getProfile().get(0).getValue(); 512 if (map.containsKey(url)) { 513 ExtensionUsage exd = map.get(url); 514 exd.minSuperset = unionMin(exd.defn.current().getMin(), ex.current().getMin()); 515 exd.maxSuperset = unionMax(exd.defn.current().getMax(), ex.current().getMax()); 516 exd.minSubset = intersectMin(exd.defn.current().getMin(), ex.current().getMin()); 517 exd.maxSubset = intersectMax(exd.defn.current().getMax(), ex.current().getMax()); 518 exd.both = true; 519 outcome.rule(subset, exd.maxSubset.equals("*") || Integer.parseInt(exd.maxSubset) >= exd.minSubset, path, 520 "Cardinality Mismatch on extension: " + card(exd.defn) + "/" + card(ex)); 521 } else { 522 map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax())); 523 } 524 } 525 List<String> names = new ArrayList<String>(); 526 names.addAll(map.keySet()); 527 Collections.sort(names); 528 for (String name : names) { 529 ExtensionUsage exd = map.get(name); 530 if (exd.both) 531 outcome.subset.getSnapshot().getElement() 532 .add(exd.defn.current().copy().setMin(exd.minSubset).setMax(exd.maxSubset)); 533 outcome.superset.getSnapshot().getElement() 534 .add(exd.defn.current().copy().setMin(exd.minSuperset).setMax(exd.maxSuperset)); 535 } 536 return true; 537 } 538 539 private boolean isExtension(String path) { 540 return path.endsWith(".extension") || path.endsWith(".modifierExtension"); 541 } 542 543 private boolean compareChildren(ElementDefinition ed, ProfileComparison outcome, String path, 544 DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { 545 List<DefinitionNavigator> lc = left.children(); 546 List<DefinitionNavigator> rc = right.children(); 547 // it's possible that one of these profiles walks into a data type and the other 548 // doesn't 549 // if it does, we have to load the children for that data into the profile that 550 // doesn't 551 // walk into it 552 if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 553 && left.hasTypeChildren(right.current().getType().get(0))) 554 lc = left.childrenFromType(right.current().getType().get(0)); 555 if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 556 && right.hasTypeChildren(left.current().getType().get(0))) 557 rc = right.childrenFromType(left.current().getType().get(0)); 558 if (lc.size() != rc.size()) { 559 outcome.messages.add(new ValidationMessage( 560 Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Different number of children at " + path 561 + " (" + Integer.toString(lc.size()) + "/" + Integer.toString(rc.size()) + ")", 562 ValidationMessage.IssueSeverity.ERROR)); 563 status(ed, ProfileUtilities.STATUS_ERROR); 564 return false; 565 } else { 566 for (int i = 0; i < lc.size(); i++) { 567 DefinitionNavigator l = lc.get(i); 568 DefinitionNavigator r = rc.get(i); 569 String cpath = comparePaths(l.path(), r.path(), path, l.nameTail(), r.nameTail()); 570 if (cpath != null) { 571 if (!compareElements(outcome, cpath, l, r)) 572 return false; 573 } else { 574 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, 575 path, "Different path at " + path + "[" + Integer.toString(i) + "] (" + l.path() + "/" + r.path() + ")", 576 ValidationMessage.IssueSeverity.ERROR)); 577 status(ed, ProfileUtilities.STATUS_ERROR); 578 return false; 579 } 580 } 581 } 582 return true; 583 } 584 585 private String comparePaths(String path1, String path2, String path, String tail1, String tail2) { 586 if (tail1.equals(tail2)) { 587 return path + "." + tail1; 588 } else if (tail1.endsWith("[x]") && tail2.startsWith(tail1.substring(0, tail1.length() - 3))) { 589 return path + "." + tail1; 590 } else if (tail2.endsWith("[x]") && tail1.startsWith(tail2.substring(0, tail2.length() - 3))) { 591 return path + "." + tail2; 592 } else 593 return null; 594 } 595 596 private boolean compareBindings(ProfileComparison outcome, ElementDefinition subset, ElementDefinition superset, 597 String path, ElementDefinition lDef, ElementDefinition rDef) throws FHIRFormatError { 598 assert (lDef.hasBinding() || rDef.hasBinding()); 599 if (!lDef.hasBinding()) { 600 subset.setBinding(rDef.getBinding()); 601 // technically, the super set is unbound, but that's not very useful - so we use 602 // the provided on as an example 603 superset.setBinding(rDef.getBinding().copy()); 604 superset.getBinding().setStrength(BindingStrength.EXAMPLE); 605 return true; 606 } 607 if (!rDef.hasBinding()) { 608 subset.setBinding(lDef.getBinding()); 609 superset.setBinding(lDef.getBinding().copy()); 610 superset.getBinding().setStrength(BindingStrength.EXAMPLE); 611 return true; 612 } 613 ElementDefinitionBindingComponent left = lDef.getBinding(); 614 ElementDefinitionBindingComponent right = rDef.getBinding(); 615 if (Base.compareDeep(left, right, false)) { 616 subset.setBinding(left); 617 superset.setBinding(right); 618 } 619 620 // if they're both examples/preferred then: 621 // subset: left wins if they're both the same 622 // superset: 623 if (isPreferredOrExample(left) && isPreferredOrExample(right)) { 624 if (right.getStrength() == BindingStrength.PREFERRED && left.getStrength() == BindingStrength.EXAMPLE 625 && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { 626 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 627 "Example/preferred bindings differ at " + path + " using binding from " + outcome.rightName(), 628 ValidationMessage.IssueSeverity.INFORMATION)); 629 status(subset, ProfileUtilities.STATUS_HINT); 630 subset.setBinding(right); 631 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 632 } else { 633 if ((right.getStrength() != BindingStrength.EXAMPLE || left.getStrength() != BindingStrength.EXAMPLE) 634 && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { 635 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, 636 path, "Example/preferred bindings differ at " + path + " using binding from " + outcome.leftName(), 637 ValidationMessage.IssueSeverity.INFORMATION)); 638 status(subset, ProfileUtilities.STATUS_HINT); 639 } 640 subset.setBinding(left); 641 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 642 } 643 return true; 644 } 645 // if either of them are extensible/required, then it wins 646 if (isPreferredOrExample(left)) { 647 subset.setBinding(right); 648 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 649 return true; 650 } 651 if (isPreferredOrExample(right)) { 652 subset.setBinding(left); 653 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 654 return true; 655 } 656 657 // ok, both are extensible or required. 658 ElementDefinitionBindingComponent subBinding = new ElementDefinitionBindingComponent(); 659 subset.setBinding(subBinding); 660 ElementDefinitionBindingComponent superBinding = new ElementDefinitionBindingComponent(); 661 superset.setBinding(superBinding); 662 subBinding 663 .setDescription(mergeText(subset, outcome, path, "description", left.getDescription(), right.getDescription())); 664 superBinding 665 .setDescription(mergeText(subset, outcome, null, "description", left.getDescription(), right.getDescription())); 666 if (left.getStrength() == BindingStrength.REQUIRED || right.getStrength() == BindingStrength.REQUIRED) 667 subBinding.setStrength(BindingStrength.REQUIRED); 668 else 669 subBinding.setStrength(BindingStrength.EXTENSIBLE); 670 if (left.getStrength() == BindingStrength.EXTENSIBLE || right.getStrength() == BindingStrength.EXTENSIBLE) 671 superBinding.setStrength(BindingStrength.EXTENSIBLE); 672 else 673 superBinding.setStrength(BindingStrength.REQUIRED); 674 675 if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { 676 subBinding.setValueSet(left.getValueSet()); 677 superBinding.setValueSet(left.getValueSet()); 678 return true; 679 } else if (!left.hasValueSet()) { 680 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 681 "No left Value set at " + path, ValidationMessage.IssueSeverity.ERROR)); 682 return true; 683 } else if (!right.hasValueSet()) { 684 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 685 "No right Value set at " + path, ValidationMessage.IssueSeverity.ERROR)); 686 return true; 687 } else { 688 // ok, now we compare the value sets. This may be unresolvable. 689 ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); 690 ValueSet rvs = resolveVS(outcome.right, right.getValueSet()); 691 if (lvs == null) { 692 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 693 "Unable to resolve left value set " + left.getValueSet().toString() + " at " + path, 694 ValidationMessage.IssueSeverity.ERROR)); 695 return true; 696 } else if (rvs == null) { 697 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 698 "Unable to resolve right value set " + right.getValueSet().toString() + " at " + path, 699 ValidationMessage.IssueSeverity.ERROR)); 700 return true; 701 } else { 702 // first, we'll try to do it by definition 703 ValueSet cvs = intersectByDefinition(lvs, rvs); 704 if (cvs == null) { 705 // if that didn't work, we'll do it by expansion 706 ValueSetExpansionOutcome le; 707 ValueSetExpansionOutcome re; 708 try { 709 le = context.expandVS(lvs, true, false); 710 re = context.expandVS(rvs, true, false); 711 if (!closed(le.getValueset()) || !closed(re.getValueset())) 712 throw new DefinitionException("unclosed value sets are not handled yet"); 713 cvs = intersectByExpansion(lvs, rvs); 714 if (!cvs.getCompose().hasInclude()) { 715 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, 716 path, "The value sets " + lvs.getUrl() + " and " + rvs.getUrl() + " do not intersect", 717 ValidationMessage.IssueSeverity.ERROR)); 718 status(subset, ProfileUtilities.STATUS_ERROR); 719 return false; 720 } 721 } catch (Exception e) { 722 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, 723 path, "Unable to expand or process value sets " + lvs.getUrl() + " and " + rvs.getUrl() + ": " 724 + e.getMessage(), 725 ValidationMessage.IssueSeverity.ERROR)); 726 status(subset, ProfileUtilities.STATUS_ERROR); 727 return false; 728 } 729 } 730 subBinding.setValueSet("#" + addValueSet(cvs)); 731 superBinding.setValueSet("#" + addValueSet(unite(superset, outcome, path, lvs, rvs))); 732 } 733 } 734 return false; 735 } 736 737 private ElementDefinitionBindingComponent unionBindings(ElementDefinition ed, ProfileComparison outcome, String path, 738 ElementDefinitionBindingComponent left, ElementDefinitionBindingComponent right) throws FHIRFormatError { 739 ElementDefinitionBindingComponent union = new ElementDefinitionBindingComponent(); 740 if (left.getStrength().compareTo(right.getStrength()) < 0) 741 union.setStrength(left.getStrength()); 742 else 743 union.setStrength(right.getStrength()); 744 union.setDescription( 745 mergeText(ed, outcome, path, "binding.description", left.getDescription(), right.getDescription())); 746 if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) 747 union.setValueSet(left.getValueSet()); 748 else { 749 ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); 750 ValueSet rvs = resolveVS(outcome.left, right.getValueSet()); 751 if (lvs != null && rvs != null) 752 union.setValueSet("#" + addValueSet(unite(ed, outcome, path, lvs, rvs))); 753 else if (lvs != null) 754 union.setValueSet("#" + addValueSet(lvs)); 755 else if (rvs != null) 756 union.setValueSet("#" + addValueSet(rvs)); 757 } 758 return union; 759 } 760 761 private ValueSet unite(ElementDefinition ed, ProfileComparison outcome, String path, ValueSet lvs, ValueSet rvs) { 762 ValueSet vs = new ValueSet(); 763 if (lvs.hasCompose()) { 764 for (ConceptSetComponent inc : lvs.getCompose().getInclude()) 765 vs.getCompose().getInclude().add(inc); 766 if (lvs.getCompose().hasExclude()) { 767 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 768 "The value sets " + lvs.getUrl() 769 + " has exclude statements, and no union involving it can be correctly determined", 770 ValidationMessage.IssueSeverity.ERROR)); 771 status(ed, ProfileUtilities.STATUS_ERROR); 772 } 773 } 774 if (rvs.hasCompose()) { 775 for (ConceptSetComponent inc : rvs.getCompose().getInclude()) 776 if (!mergeIntoExisting(vs.getCompose().getInclude(), inc)) 777 vs.getCompose().getInclude().add(inc); 778 if (rvs.getCompose().hasExclude()) { 779 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 780 "The value sets " + lvs.getUrl() 781 + " has exclude statements, and no union involving it can be correctly determined", 782 ValidationMessage.IssueSeverity.ERROR)); 783 status(ed, ProfileUtilities.STATUS_ERROR); 784 } 785 } 786 return vs; 787 } 788 789 private boolean mergeIntoExisting(List<ConceptSetComponent> include, ConceptSetComponent inc) { 790 for (ConceptSetComponent dst : include) { 791 if (Base.compareDeep(dst, inc, false)) 792 return true; // they're actually the same 793 if (dst.getSystem().equals(inc.getSystem())) { 794 if (inc.hasFilter() || dst.hasFilter()) { 795 return false; // just add the new one as a a parallel 796 } else if (inc.hasConcept() && dst.hasConcept()) { 797 for (ConceptReferenceComponent cc : inc.getConcept()) { 798 boolean found = false; 799 for (ConceptReferenceComponent dd : dst.getConcept()) { 800 if (dd.getCode().equals(cc.getCode())) 801 found = true; 802 if (found) { 803 if (cc.hasDisplay() && !dd.hasDisplay()) 804 dd.setDisplay(cc.getDisplay()); 805 break; 806 } 807 } 808 if (!found) 809 dst.getConcept().add(cc.copy()); 810 } 811 } else 812 dst.getConcept().clear(); // one of them includes the entire code system 813 } 814 } 815 return false; 816 } 817 818 private ValueSet resolveVS(StructureDefinition ctxtLeft, String vsRef) { 819 if (vsRef == null) 820 return null; 821 return context.fetchResource(ValueSet.class, vsRef); 822 } 823 824 private ValueSet intersectByDefinition(ValueSet lvs, ValueSet rvs) { 825 // this is just a stub. The idea is that we try to avoid expanding big open 826 // value sets from SCT, RxNorm, LOINC. 827 // there's a bit of long hand logic coming here, but that's ok. 828 return null; 829 } 830 831 private ValueSet intersectByExpansion(ValueSet lvs, ValueSet rvs) { 832 // this is pretty straight forward - we intersect the lists, and build a compose 833 // out of the intersection 834 ValueSet vs = new ValueSet(); 835 vs.setStatus(PublicationStatus.DRAFT); 836 837 Map<String, ValueSetExpansionContainsComponent> left = new HashMap<String, ValueSetExpansionContainsComponent>(); 838 scan(lvs.getExpansion().getContains(), left); 839 Map<String, ValueSetExpansionContainsComponent> right = new HashMap<String, ValueSetExpansionContainsComponent>(); 840 scan(rvs.getExpansion().getContains(), right); 841 Map<String, ConceptSetComponent> inc = new HashMap<String, ConceptSetComponent>(); 842 843 for (String s : left.keySet()) { 844 if (right.containsKey(s)) { 845 ValueSetExpansionContainsComponent cc = left.get(s); 846 ConceptSetComponent c = inc.get(cc.getSystem()); 847 if (c == null) { 848 c = vs.getCompose().addInclude().setSystem(cc.getSystem()); 849 inc.put(cc.getSystem(), c); 850 } 851 c.addConcept().setCode(cc.getCode()).setDisplay(cc.getDisplay()); 852 } 853 } 854 return vs; 855 } 856 857 private void scan(List<ValueSetExpansionContainsComponent> list, 858 Map<String, ValueSetExpansionContainsComponent> map) { 859 for (ValueSetExpansionContainsComponent cc : list) { 860 if (cc.hasSystem() && cc.hasCode()) { 861 String s = cc.getSystem() + "::" + cc.getCode(); 862 if (!map.containsKey(s)) 863 map.put(s, cc); 864 } 865 if (cc.hasContains()) 866 scan(cc.getContains(), map); 867 } 868 } 869 870 private boolean closed(ValueSet vs) { 871 return !ToolingExtensions.findBooleanExtension(vs.getExpansion(), ToolingExtensions.EXT_UNCLOSED); 872 } 873 874 private boolean isPreferredOrExample(ElementDefinitionBindingComponent binding) { 875 return binding.getStrength() == BindingStrength.EXAMPLE || binding.getStrength() == BindingStrength.PREFERRED; 876 } 877 878 private Collection<? extends TypeRefComponent> intersectTypes(ElementDefinition ed, ProfileComparison outcome, 879 String path, List<TypeRefComponent> left, List<TypeRefComponent> right) 880 throws DefinitionException, IOException, FHIRFormatError { 881 List<TypeRefComponent> result = new ArrayList<TypeRefComponent>(); 882 for (TypeRefComponent l : left) { 883 if (l.hasAggregation()) 884 throw new DefinitionException("Aggregation not supported: " + path); 885 boolean pfound = false; 886 boolean tfound = false; 887 TypeRefComponent c = l.copy(); 888 for (TypeRefComponent r : right) { 889 if (r.hasAggregation()) 890 throw new DefinitionException("Aggregation not supported: " + path); 891 if (!l.hasProfile() && !r.hasProfile()) { 892 pfound = true; 893 } else if (!r.hasProfile()) { 894 pfound = true; 895 } else if (!l.hasProfile()) { 896 pfound = true; 897 c.setProfile(r.getProfile()); 898 } else { 899 StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), 900 outcome.leftName()); 901 StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), 902 outcome.rightName()); 903 if (sdl != null && sdr != null) { 904 if (sdl == sdr) { 905 pfound = true; 906 } else if (derivesFrom(sdl, sdr)) { 907 pfound = true; 908 } else if (derivesFrom(sdr, sdl)) { 909 c.setProfile(r.getProfile()); 910 pfound = true; 911 } else if (sdl.getType().equals(sdr.getType())) { 912 ProfileComparison comp = compareProfiles(sdl, sdr); 913 if (comp.getSubset() != null) { 914 pfound = true; 915 c.addProfile("#" + comp.id); 916 } 917 } 918 } 919 } 920 if (!l.hasTargetProfile() && !r.hasTargetProfile()) { 921 tfound = true; 922 } else if (!r.hasTargetProfile()) { 923 tfound = true; 924 } else if (!l.hasTargetProfile()) { 925 tfound = true; 926 c.setTargetProfile(r.getTargetProfile()); 927 } else { 928 StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), 929 outcome.leftName()); 930 StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), 931 outcome.rightName()); 932 if (sdl != null && sdr != null) { 933 if (sdl == sdr) { 934 tfound = true; 935 } else if (derivesFrom(sdl, sdr)) { 936 tfound = true; 937 } else if (derivesFrom(sdr, sdl)) { 938 c.setTargetProfile(r.getTargetProfile()); 939 tfound = true; 940 } else if (sdl.getType().equals(sdr.getType())) { 941 ProfileComparison comp = compareProfiles(sdl, sdr); 942 if (comp.getSubset() != null) { 943 tfound = true; 944 c.addTargetProfile("#" + comp.id); 945 } 946 } 947 } 948 } 949 } 950 if (pfound && tfound) 951 result.add(c); 952 } 953 return result; 954 } 955 956 private StructureDefinition resolveProfile(ElementDefinition ed, ProfileComparison outcome, String path, String url, 957 String name) { 958 StructureDefinition res = context.fetchResource(StructureDefinition.class, url); 959 if (res == null) { 960 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, 961 path, "Unable to resolve profile " + url + " in profile " + name, ValidationMessage.IssueSeverity.WARNING)); 962 status(ed, ProfileUtilities.STATUS_HINT); 963 } 964 return res; 965 } 966 967 private Collection<? extends TypeRefComponent> unionTypes(String path, List<TypeRefComponent> left, 968 List<TypeRefComponent> right) throws DefinitionException, IOException, FHIRFormatError { 969 List<TypeRefComponent> result = new ArrayList<TypeRefComponent>(); 970 for (TypeRefComponent l : left) 971 checkAddTypeUnion(path, result, l); 972 for (TypeRefComponent r : right) 973 checkAddTypeUnion(path, result, r); 974 return result; 975 } 976 977 private void checkAddTypeUnion(String path, List<TypeRefComponent> results, TypeRefComponent nw) 978 throws DefinitionException, IOException, FHIRFormatError { 979 boolean pfound = false; 980 boolean tfound = false; 981 nw = nw.copy(); 982 if (nw.hasAggregation()) 983 throw new DefinitionException("Aggregation not supported: " + path); 984 for (TypeRefComponent ex : results) { 985 if (Utilities.equals(ex.getWorkingCode(), nw.getWorkingCode())) { 986 if (!ex.hasProfile() && !nw.hasProfile()) 987 pfound = true; 988 else if (!ex.hasProfile()) { 989 pfound = true; 990 } else if (!nw.hasProfile()) { 991 pfound = true; 992 ex.setProfile(null); 993 } else { 994 // both have profiles. Is one derived from the other? 995 StructureDefinition sdex = context.fetchResource(StructureDefinition.class, 996 ex.getProfile().get(0).getValue()); 997 StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, 998 nw.getProfile().get(0).getValue()); 999 if (sdex != null && sdnw != null) { 1000 if (sdex == sdnw) { 1001 pfound = true; 1002 } else if (derivesFrom(sdex, sdnw)) { 1003 ex.setProfile(nw.getProfile()); 1004 pfound = true; 1005 } else if (derivesFrom(sdnw, sdex)) { 1006 pfound = true; 1007 } else if (sdnw.getSnapshot().getElement().get(0).getPath() 1008 .equals(sdex.getSnapshot().getElement().get(0).getPath())) { 1009 ProfileComparison comp = compareProfiles(sdex, sdnw); 1010 if (comp.getSuperset() != null) { 1011 pfound = true; 1012 ex.addProfile("#" + comp.id); 1013 } 1014 } 1015 } 1016 } 1017 if (!ex.hasTargetProfile() && !nw.hasTargetProfile()) 1018 tfound = true; 1019 else if (!ex.hasTargetProfile()) { 1020 tfound = true; 1021 } else if (!nw.hasTargetProfile()) { 1022 tfound = true; 1023 ex.setTargetProfile(null); 1024 } else { 1025 // both have profiles. Is one derived from the other? 1026 StructureDefinition sdex = context.fetchResource(StructureDefinition.class, 1027 ex.getTargetProfile().get(0).getValue()); 1028 StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, 1029 nw.getTargetProfile().get(0).getValue()); 1030 if (sdex != null && sdnw != null) { 1031 if (sdex == sdnw) { 1032 tfound = true; 1033 } else if (derivesFrom(sdex, sdnw)) { 1034 ex.setTargetProfile(nw.getTargetProfile()); 1035 tfound = true; 1036 } else if (derivesFrom(sdnw, sdex)) { 1037 tfound = true; 1038 } else if (sdnw.getSnapshot().getElement().get(0).getPath() 1039 .equals(sdex.getSnapshot().getElement().get(0).getPath())) { 1040 ProfileComparison comp = compareProfiles(sdex, sdnw); 1041 if (comp.getSuperset() != null) { 1042 tfound = true; 1043 ex.addTargetProfile("#" + comp.id); 1044 } 1045 } 1046 } 1047 } 1048 } 1049 } 1050 if (!tfound || !pfound) 1051 results.add(nw); 1052 } 1053 1054 private boolean derivesFrom(StructureDefinition left, StructureDefinition right) { 1055 // left derives from right if it's base is the same as right 1056 // todo: recursive... 1057 return left.hasBaseDefinition() && left.getBaseDefinition().equals(right.getUrl()); 1058 } 1059 1060 private String mergeText(ElementDefinition ed, ProfileComparison outcome, String path, String name, String left, 1061 String right) { 1062 if (left == null && right == null) 1063 return null; 1064 if (left == null) 1065 return right; 1066 if (right == null) 1067 return left; 1068 if (left.equalsIgnoreCase(right)) 1069 return left; 1070 if (path != null) { 1071 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, 1072 path, "Elements differ in definition for " + name + ":\r\n \"" + left + "\"\r\n \"" + right + "\"", 1073 "Elements differ in definition for " + name + ":<br/>\"" + Utilities.escapeXml(left) + "\"<br/>\"" 1074 + Utilities.escapeXml(right) + "\"", 1075 ValidationMessage.IssueSeverity.INFORMATION)); 1076 status(ed, ProfileUtilities.STATUS_HINT); 1077 } 1078 return "left: " + left + "; right: " + right; 1079 } 1080 1081 private List<Coding> mergeCodings(List<Coding> left, List<Coding> right) { 1082 List<Coding> result = new ArrayList<Coding>(); 1083 result.addAll(left); 1084 for (Coding c : right) { 1085 boolean found = false; 1086 for (Coding ct : left) 1087 if (Utilities.equals(c.getSystem(), ct.getSystem()) && Utilities.equals(c.getCode(), ct.getCode())) 1088 found = true; 1089 if (!found) 1090 result.add(c); 1091 } 1092 return result; 1093 } 1094 1095 private List<StringType> mergeStrings(List<StringType> left, List<StringType> right) { 1096 List<StringType> result = new ArrayList<StringType>(); 1097 result.addAll(left); 1098 for (StringType c : right) { 1099 boolean found = false; 1100 for (StringType ct : left) 1101 if (Utilities.equals(c.getValue(), ct.getValue())) 1102 found = true; 1103 if (!found) 1104 result.add(c); 1105 } 1106 return result; 1107 } 1108 1109 private List<ElementDefinitionMappingComponent> mergeMappings(List<ElementDefinitionMappingComponent> left, 1110 List<ElementDefinitionMappingComponent> right) { 1111 List<ElementDefinitionMappingComponent> result = new ArrayList<ElementDefinitionMappingComponent>(); 1112 result.addAll(left); 1113 for (ElementDefinitionMappingComponent c : right) { 1114 boolean found = false; 1115 for (ElementDefinitionMappingComponent ct : left) 1116 if (Utilities.equals(c.getIdentity(), ct.getIdentity()) && Utilities.equals(c.getLanguage(), ct.getLanguage()) 1117 && Utilities.equals(c.getMap(), ct.getMap())) 1118 found = true; 1119 if (!found) 1120 result.add(c); 1121 } 1122 return result; 1123 } 1124 1125 // we can't really know about constraints. We create warnings, and collate them 1126 private List<ElementDefinitionConstraintComponent> unionConstraints(ElementDefinition ed, ProfileComparison outcome, 1127 String path, List<ElementDefinitionConstraintComponent> left, List<ElementDefinitionConstraintComponent> right) { 1128 List<ElementDefinitionConstraintComponent> result = new ArrayList<ElementDefinitionConstraintComponent>(); 1129 for (ElementDefinitionConstraintComponent l : left) { 1130 boolean found = false; 1131 for (ElementDefinitionConstraintComponent r : right) 1132 if (Utilities.equals(r.getId(), l.getId()) 1133 || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) 1134 found = true; 1135 if (!found) { 1136 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 1137 "StructureDefinition " + outcome.leftName() + " has a constraint that is not found in " 1138 + outcome.rightName() + " and it is uncertain whether they are compatible (" + l.getXpath() + ")", 1139 ValidationMessage.IssueSeverity.INFORMATION)); 1140 status(ed, ProfileUtilities.STATUS_WARNING); 1141 } 1142 result.add(l); 1143 } 1144 for (ElementDefinitionConstraintComponent r : right) { 1145 boolean found = false; 1146 for (ElementDefinitionConstraintComponent l : left) 1147 if (Utilities.equals(r.getId(), l.getId()) 1148 || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) 1149 found = true; 1150 if (!found) { 1151 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, 1152 "StructureDefinition " + outcome.rightName() + " has a constraint that is not found in " 1153 + outcome.leftName() + " and it is uncertain whether they are compatible (" + r.getXpath() + ")", 1154 ValidationMessage.IssueSeverity.INFORMATION)); 1155 status(ed, ProfileUtilities.STATUS_WARNING); 1156 result.add(r); 1157 } 1158 } 1159 return result; 1160 } 1161 1162 private List<ElementDefinitionConstraintComponent> intersectConstraints(String path, 1163 List<ElementDefinitionConstraintComponent> left, List<ElementDefinitionConstraintComponent> right) { 1164 List<ElementDefinitionConstraintComponent> result = new ArrayList<ElementDefinitionConstraintComponent>(); 1165 for (ElementDefinitionConstraintComponent l : left) { 1166 boolean found = false; 1167 for (ElementDefinitionConstraintComponent r : right) 1168 if (Utilities.equals(r.getId(), l.getId()) 1169 || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) 1170 found = true; 1171 if (found) 1172 result.add(l); 1173 } 1174 return result; 1175 } 1176 1177 private String card(DefinitionNavigator defn) { 1178 return Integer.toString(defn.current().getMin()) + ".." + defn.current().getMax(); 1179 } 1180 1181 private String typeCode(DefinitionNavigator defn) { 1182 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 1183 for (TypeRefComponent t : defn.current().getType()) 1184 b.append(t.getWorkingCode() + (t.hasProfile() ? "(" + t.getProfile() + ")" : "") 1185 + (t.hasTargetProfile() ? "(" + t.getTargetProfile() + ")" : "")); // todo: other properties 1186 return b.toString(); 1187 } 1188 1189 private int intersectMin(int left, int right) { 1190 if (left > right) 1191 return left; 1192 else 1193 return right; 1194 } 1195 1196 private int unionMin(int left, int right) { 1197 if (left > right) 1198 return right; 1199 else 1200 return left; 1201 } 1202 1203 private String intersectMax(String left, String right) { 1204 int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); 1205 int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); 1206 if (l < r) 1207 return left; 1208 else 1209 return right; 1210 } 1211 1212 private String unionMax(String left, String right) { 1213 int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); 1214 int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); 1215 if (l < r) 1216 return right; 1217 else 1218 return left; 1219 } 1220 1221 private IntegerType intersectMaxLength(int left, int right) { 1222 if (left == 0) 1223 left = Integer.MAX_VALUE; 1224 if (right == 0) 1225 right = Integer.MAX_VALUE; 1226 if (left < right) 1227 return left == Integer.MAX_VALUE ? null : new IntegerType(left); 1228 else 1229 return right == Integer.MAX_VALUE ? null : new IntegerType(right); 1230 } 1231 1232 private IntegerType unionMaxLength(int left, int right) { 1233 if (left == 0) 1234 left = Integer.MAX_VALUE; 1235 if (right == 0) 1236 right = Integer.MAX_VALUE; 1237 if (left < right) 1238 return right == Integer.MAX_VALUE ? null : new IntegerType(right); 1239 else 1240 return left == Integer.MAX_VALUE ? null : new IntegerType(left); 1241 } 1242 1243 public String addValueSet(ValueSet cvs) { 1244 String id = Integer.toString(valuesets.size() + 1); 1245 cvs.setId(id); 1246 valuesets.add(cvs); 1247 return id; 1248 } 1249 1250 public String getId() { 1251 return id; 1252 } 1253 1254 public void setId(String id) { 1255 this.id = id; 1256 } 1257 1258 public String getTitle() { 1259 return title; 1260 } 1261 1262 public void setTitle(String title) { 1263 this.title = title; 1264 } 1265 1266 public String getLeftLink() { 1267 return leftLink; 1268 } 1269 1270 public void setLeftLink(String leftLink) { 1271 this.leftLink = leftLink; 1272 } 1273 1274 public String getLeftName() { 1275 return leftName; 1276 } 1277 1278 public void setLeftName(String leftName) { 1279 this.leftName = leftName; 1280 } 1281 1282 public String getRightLink() { 1283 return rightLink; 1284 } 1285 1286 public void setRightLink(String rightLink) { 1287 this.rightLink = rightLink; 1288 } 1289 1290 public String getRightName() { 1291 return rightName; 1292 } 1293 1294 public void setRightName(String rightName) { 1295 this.rightName = rightName; 1296 } 1297 1298 private String genPCLink(String leftName, String leftLink) { 1299 return "<a href=\"" + leftLink + "\">" + Utilities.escapeXml(leftName) + "</a>"; 1300 } 1301 1302 private String genPCTable() { 1303 StringBuilder b = new StringBuilder(); 1304 1305 b.append("<table class=\"grid\">\r\n"); 1306 b.append("<tr>"); 1307 b.append(" <td><b>Left</b></td>"); 1308 b.append(" <td><b>Right</b></td>"); 1309 b.append(" <td><b>Comparison</b></td>"); 1310 b.append(" <td><b>Error #</b></td>"); 1311 b.append(" <td><b>Warning #</b></td>"); 1312 b.append(" <td><b>Hint #</b></td>"); 1313 b.append("</tr>"); 1314 1315 for (ProfileComparison cmp : getComparisons()) { 1316 b.append("<tr>"); 1317 b.append(" <td><a href=\"" + cmp.getLeft().getUserString("path") + "\">" 1318 + Utilities.escapeXml(cmp.getLeft().getName()) + "</a></td>"); 1319 b.append(" <td><a href=\"" + cmp.getRight().getUserString("path") + "\">" 1320 + Utilities.escapeXml(cmp.getRight().getName()) + "</a></td>"); 1321 b.append(" <td><a href=\"" + getId() + "." + cmp.getId() + ".html\">Click Here</a></td>"); 1322 b.append(" <td>" + cmp.getErrorCount() + "</td>"); 1323 b.append(" <td>" + cmp.getWarningCount() + "</td>"); 1324 b.append(" <td>" + cmp.getHintCount() + "</td>"); 1325 b.append("</tr>"); 1326 } 1327 b.append("</table>\r\n"); 1328 1329 return b.toString(); 1330 } 1331 1332 public String generate(String dest) throws IOException { 1333 // ok, all compared; now produce the output 1334 // first page we produce is simply the index 1335 Map<String, String> vars = new HashMap<String, String>(); 1336 vars.put("title", getTitle()); 1337 vars.put("left", genPCLink(getLeftName(), getLeftLink())); 1338 vars.put("right", genPCLink(getRightName(), getRightLink())); 1339 vars.put("table", genPCTable()); 1340 producePage(summaryTemplate(), Utilities.path(dest, getId() + ".html"), vars); 1341 1342// page.log(" ... generate", LogMessageType.Process); 1343// String src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison-set.html"); 1344// src = page.processPageIncludes(n+".html", src, "?type", null, "??path", null, null, "Comparison", pc, null, null, page.getDefinitions().getWorkgroups().get("fhir")); 1345// TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+".html")); 1346// cachePage(n + ".html", src, "Comparison "+pc.getTitle(), false); 1347// 1348// // then we produce a comparison page for each pair 1349// for (ProfileComparison cmp : pc.getComparisons()) { 1350// src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison.html"); 1351// src = page.processPageIncludes(n+"."+cmp.getId()+".html", src, "?type", null, "??path", null, null, "Comparison", cmp, null, null, page.getDefinitions().getWorkgroups().get("fhir")); 1352// TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+"."+cmp.getId()+".html")); 1353// cachePage(n +"."+cmp.getId()+".html", src, "Comparison "+pc.getTitle(), false); 1354// } 1355// // and also individual pages for each pair outcome 1356// // then we produce value set pages for each value set 1357// 1358// // TODO Auto-generated method stub 1359 return Utilities.path(dest, getId() + ".html"); 1360 } 1361 1362 private void producePage(String src, String path, Map<String, String> vars) throws IOException { 1363 while (src.contains("[%")) { 1364 int i1 = src.indexOf("[%"); 1365 int i2 = src.substring(i1).indexOf("%]") + i1; 1366 String s1 = src.substring(0, i1); 1367 String s2 = src.substring(i1 + 2, i2).trim(); 1368 String s3 = src.substring(i2 + 2); 1369 String v = vars.containsKey(s2) ? vars.get(s2) : "???"; 1370 src = s1 + v + s3; 1371 } 1372 TextFile.stringToFile(src, path); 1373 } 1374 1375 private String summaryTemplate() throws IOException { 1376 return cachedFetch("04a9d69a-47f2-4250-8645-bf5d880a8eaa-1.fhir-template", 1377 "http://build.fhir.org/template-comparison-set.html.template"); 1378 } 1379 1380 private String cachedFetch(String id, String source) throws IOException { 1381 String tmpDir = System.getProperty("java.io.tmpdir"); 1382 String local = Utilities.path(tmpDir, id); 1383 File f = ManagedFileAccess.file(local); 1384 if (f.exists()) 1385 return TextFile.fileToString(f); 1386 1387 HTTPResult res = ManagedWebAccess.get(source); 1388 res.checkThrowException(); 1389 String result = TextFile.bytesToString(res.getContent()); 1390 TextFile.stringToFile(result, f); 1391 return result; 1392 } 1393 1394}