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