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