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