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