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